carrick 0.3.123 → 0.3.125
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/capture/check-classify.js +11 -5
- package/sidecar/dist/src/capture/check.js +8 -1
- package/sidecar/dist/src/capture/deep-walk.d.ts +31 -0
- package/sidecar/dist/src/capture/deep-walk.js +43 -1
- package/sidecar/dist/src/capture/index.d.ts +1 -1
- package/sidecar/dist/src/capture/index.js +1 -1
- package/sidecar/dist/src/capture/machinery.js +6 -0
- package/sidecar/dist/src/capture/service-config.d.ts +11 -0
- package/sidecar/dist/src/capture/service-config.js +40 -0
- package/sidecar/dist/src/index.js +1 -1
- package/sidecar/dist/src/library-claims.d.ts +1 -1
- package/sidecar/dist/src/library-claims.js +170 -11
- package/sidecar/dist/src/position-rows.d.ts +44 -0
- package/sidecar/dist/src/position-rows.js +6 -4
- package/sidecar/dist/src/project-loader.d.ts +16 -0
- package/sidecar/dist/src/project-loader.js +51 -4
- package/sidecar/dist/src/type-inferrer.d.ts +56 -15
- package/sidecar/dist/src/type-inferrer.js +328 -67
- package/sidecar/dist/src/types.d.ts +32 -0
- package/sidecar/dist/src/validators.d.ts +392 -60
- package/sidecar/dist/src/validators.js +14 -0
|
@@ -115,6 +115,12 @@ export const MACHINERY_MEMBER_INDICATORS = new Set([
|
|
|
115
115
|
'clone',
|
|
116
116
|
'json',
|
|
117
117
|
'statusText',
|
|
118
|
+
// FROZEN (old path, 2026-10-21): the reply-object half below only (the
|
|
119
|
+
// fetch half above and the threshold stay). Deletions and safety fixes
|
|
120
|
+
// only. Fixes for wrong or missing types go in labelled reply objects
|
|
121
|
+
// (carrick#2472 C4) and the language library rows (carrick#2445). Delete
|
|
122
|
+
// when a delete-and-replay on the four repos moves no row wrongly, after
|
|
123
|
+
// carrick-cloud#2379 and carrick#2445 (carrick#2472, C6).
|
|
118
124
|
// Node http ServerResponse / reply-object surface
|
|
119
125
|
'statusCode',
|
|
120
126
|
'statusMessage',
|
|
@@ -1628,6 +1634,8 @@ export class TypeInferrer {
|
|
|
1628
1634
|
branches = [
|
|
1629
1635
|
...returnedBranches,
|
|
1630
1636
|
...statementSends
|
|
1637
|
+
// FROZEN (old path, 2026-10-21): the `statementEndsFunctionPath` test
|
|
1638
|
+
// here; see that method.
|
|
1631
1639
|
.filter((s) => s.send === locatedBranch || this.statementEndsFunctionPath(s.statement, func))
|
|
1632
1640
|
.map((s) => s.send),
|
|
1633
1641
|
];
|
|
@@ -1702,6 +1710,10 @@ export class TypeInferrer {
|
|
|
1702
1710
|
}
|
|
1703
1711
|
return sends;
|
|
1704
1712
|
}
|
|
1713
|
+
// FROZEN (old path, 2026-10-21): this method and its one use. Deletions and
|
|
1714
|
+
// safety fixes only. Fixes for wrong or missing types go in send versus
|
|
1715
|
+
// setter, read from the framework label (carrick#2472 C4). Delete when C4 is
|
|
1716
|
+
// released (carrick#2472, C6).
|
|
1705
1717
|
/**
|
|
1706
1718
|
* True when nothing in `func` runs after `statement`: a bare `return;`
|
|
1707
1719
|
* follows it, or it is the last statement of its block and the statement
|
|
@@ -1748,6 +1760,11 @@ export class TypeInferrer {
|
|
|
1748
1760
|
return false;
|
|
1749
1761
|
}
|
|
1750
1762
|
}
|
|
1763
|
+
// FROZEN (old path, 2026-10-21): this method's structural test (the whole
|
|
1764
|
+
// method is the upper bound). Deletions and safety fixes only. Fixes for
|
|
1765
|
+
// wrong or missing types go in labelled sends (carrick#2472 C4). Delete when
|
|
1766
|
+
// a delete-and-replay on the four repos moves no row wrongly, after
|
|
1767
|
+
// carrick-cloud#2379 and carrick#2445 (carrick#2472, C6).
|
|
1751
1768
|
/**
|
|
1752
1769
|
* True when `candidate` is a response send: a call or `new` whose result is
|
|
1753
1770
|
* transport machinery (by the extraction config's verified rule or the
|
|
@@ -1979,6 +1996,27 @@ export class TypeInferrer {
|
|
|
1979
1996
|
});
|
|
1980
1997
|
}
|
|
1981
1998
|
typeString = this.unwrapPromise(typeString, returnType);
|
|
1999
|
+
// carrick#2594: the call's value is untyped (`Promise<any>`,
|
|
2000
|
+
// `Promise<unknown>`), and the function it is made in states its type:
|
|
2001
|
+
// it returns the value from a function with a declared return type, holds
|
|
2002
|
+
// it in an annotated name, validates it with a schema, narrows it with a
|
|
2003
|
+
// function that returns it unchanged, casts it whole, or hands it to a
|
|
2004
|
+
// typed parameter. A function that converts the value into something new
|
|
2005
|
+
// states nothing about it, and neither does a use on a failure path.
|
|
2006
|
+
const untypedAnswer = typeString.trim() === 'any' || typeString.trim() === 'unknown';
|
|
2007
|
+
if (untypedAnswer && func && !explicitType && !unwrapResult.wasUnwrapped && !carrierPayload) {
|
|
2008
|
+
const stated = this.statedInFunction(callExpr, func, {
|
|
2009
|
+
returned: true,
|
|
2010
|
+
parameters: true,
|
|
2011
|
+
skipFailurePaths: true,
|
|
2012
|
+
});
|
|
2013
|
+
if (stated) {
|
|
2014
|
+
this.log(`Call result at ${where} is untyped, and its function states its type ` +
|
|
2015
|
+
`(${stated.how}); publishing that type`);
|
|
2016
|
+
typeString = stated.text;
|
|
2017
|
+
isExplicit = true;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1982
2020
|
// #336: consumer analogue of the #306 producer anchor. Anchor on the
|
|
1983
2021
|
// CALL's own payload (the rule-extracted Type, else the awaited call
|
|
1984
2022
|
// type), peeling array levels to the element symbol + depth. Without the
|
|
@@ -2642,18 +2680,22 @@ export class TypeInferrer {
|
|
|
2642
2680
|
isExplicit = true;
|
|
2643
2681
|
}
|
|
2644
2682
|
// carrick#1166: the read itself is untyped (`await c.req.json()` is `any`
|
|
2645
|
-
// or `unknown`), and the handler
|
|
2646
|
-
// `Schema.safeParse(body)`,
|
|
2647
|
-
//
|
|
2648
|
-
//
|
|
2683
|
+
// or `unknown`), and the handler states its type further on: it validates
|
|
2684
|
+
// it with a schema (`Schema.safeParse(body ?? {})`, whose INPUT is the
|
|
2685
|
+
// request contract, carrick#1101), assigns it to an annotated name
|
|
2686
|
+
// (`let body: T; body = await c.req.json()`), narrows it with a function
|
|
2687
|
+
// that returns it unchanged, or casts the whole value (carrick#2594).
|
|
2649
2688
|
const readType = this.unwrapPromiseType(payloadType);
|
|
2650
2689
|
if (!explicitType &&
|
|
2651
2690
|
!unwrapResult.wasUnwrapped &&
|
|
2652
2691
|
(readType.isAny() || readType.isUnknown())) {
|
|
2653
|
-
const
|
|
2692
|
+
const func = this.findContainingFunctionForNode(node);
|
|
2693
|
+
const parsedWith = func
|
|
2694
|
+
? this.statedInFunction(node, func, { returned: false, parameters: false, skipFailurePaths: false })
|
|
2695
|
+
: null;
|
|
2654
2696
|
if (parsedWith) {
|
|
2655
2697
|
this.log(`Request locator at ${request.file_path}:${request.line_number} is an untyped read ` +
|
|
2656
|
-
|
|
2698
|
+
`its handler states a type for (${parsedWith.how}); publishing that type`);
|
|
2657
2699
|
return this.declaredRequestInferredType(request, parsedWith, this.getNodeLocation(node));
|
|
2658
2700
|
}
|
|
2659
2701
|
}
|
|
@@ -5288,6 +5330,10 @@ export class TypeInferrer {
|
|
|
5288
5330
|
}
|
|
5289
5331
|
return flattened;
|
|
5290
5332
|
}
|
|
5333
|
+
// FROZEN (old path, 2026-10-21): this method only. Deletions and safety fixes
|
|
5334
|
+
// only. Fixes for wrong or missing types go in labelled sends (carrick#2472
|
|
5335
|
+
// C4). Delete when a delete-and-replay on the four repos moves no row
|
|
5336
|
+
// wrongly, after carrick-cloud#2379 and carrick#2445 (carrick#2472, C6).
|
|
5291
5337
|
/**
|
|
5292
5338
|
* The callees these returned expressions PROVE are response serialisers.
|
|
5293
5339
|
*
|
|
@@ -6585,8 +6631,10 @@ export class TypeInferrer {
|
|
|
6585
6631
|
* - a cast on it: `(await request.json()) as NewWidget`;
|
|
6586
6632
|
* - the annotation of the binding it initialises:
|
|
6587
6633
|
* `const input: NewWidget = await request.json()`;
|
|
6588
|
-
* - a
|
|
6589
|
-
* (`
|
|
6634
|
+
* - a type the handler states for the read further on
|
|
6635
|
+
* (`statedInFunction`): a schema it is handed to, the annotation of a
|
|
6636
|
+
* name it is assigned to, a pass-through function, a cast of the whole
|
|
6637
|
+
* value.
|
|
6590
6638
|
*
|
|
6591
6639
|
* The first two are `typeNodeStatedAtRead`, the reading a consumer's
|
|
6592
6640
|
* response read already gets. An annotation further out is not one of them:
|
|
@@ -6602,10 +6650,13 @@ export class TypeInferrer {
|
|
|
6602
6650
|
'reads off the platform request, as the source types that read');
|
|
6603
6651
|
return this.createInferredType(request, statedText, true, this.getNodeLocation(read));
|
|
6604
6652
|
}
|
|
6605
|
-
const
|
|
6653
|
+
const func = this.findContainingFunctionForNode(read);
|
|
6654
|
+
const validated = func
|
|
6655
|
+
? this.statedInFunction(read, func, { returned: false, parameters: false, skipFailurePaths: false })
|
|
6656
|
+
: null;
|
|
6606
6657
|
if (validated) {
|
|
6607
6658
|
this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
|
|
6608
|
-
|
|
6659
|
+
`reads off the platform request and states a type for (${validated.how}); publishing that type`);
|
|
6609
6660
|
return this.declaredRequestInferredType(request, validated, this.getNodeLocation(read));
|
|
6610
6661
|
}
|
|
6611
6662
|
return null;
|
|
@@ -7264,70 +7315,280 @@ export class TypeInferrer {
|
|
|
7264
7315
|
return values;
|
|
7265
7316
|
}
|
|
7266
7317
|
/**
|
|
7267
|
-
* The
|
|
7268
|
-
* (carrick#
|
|
7269
|
-
*
|
|
7270
|
-
*
|
|
7271
|
-
*
|
|
7272
|
-
*
|
|
7273
|
-
*
|
|
7274
|
-
* (`
|
|
7275
|
-
*
|
|
7276
|
-
*
|
|
7277
|
-
*
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7318
|
+
* The type the function a value is made in states for it, where the
|
|
7319
|
+
* compiler types the value itself `any` or `unknown` (carrick#2594), or
|
|
7320
|
+
* null.
|
|
7321
|
+
*
|
|
7322
|
+
* `value` is the located expression: a body read, or a call whose result is
|
|
7323
|
+
* untyped. The value is followed through the names that hold it: a
|
|
7324
|
+
* declaration it initialises, and a name it is assigned to later
|
|
7325
|
+
* (`let body: unknown; body = await read()`). At each place the value or
|
|
7326
|
+
* one of those names is used whole, the source can state a type:
|
|
7327
|
+
*
|
|
7328
|
+
* 0. the annotation of the name that holds it (`let body: Upload`), or the
|
|
7329
|
+
* declared return type of the function that returns it unchanged (only
|
|
7330
|
+
* with `reads.returned`);
|
|
7331
|
+
* 1. a schema it is handed to (`Schema.safeParse(body ?? {})`,
|
|
7332
|
+
* `parse(Schema, body)`): the schema's input is what a sender may send
|
|
7333
|
+
* (carrick#1166). A candidate counts only when it declares schema types,
|
|
7334
|
+
* so no method or library name is matched;
|
|
7335
|
+
* 2. a function of the project it is handed to that returns that argument
|
|
7336
|
+
* unchanged (through casts) or an empty default: its declared return
|
|
7337
|
+
* type. A function that builds a new value from its argument is a
|
|
7338
|
+
* converter, and its return type describes what it built, never the
|
|
7339
|
+
* value it was handed, so it states nothing here;
|
|
7340
|
+
* 3. a cast of the whole value (`(body as { phone: string }).phone`);
|
|
7341
|
+
* 4. the parameter of a call it is handed to (only with
|
|
7342
|
+
* `reads.parameters`), where that parameter's type is a body shape once
|
|
7343
|
+
* any callable member is dropped (a function never crosses the wire).
|
|
7344
|
+
*
|
|
7345
|
+
* The first rank with an answer decides. A placeholder (`unknown`,
|
|
7346
|
+
* `Record<string, unknown>`, any type node with `any` or `unknown` written
|
|
7347
|
+
* in it) states nothing, and two different statements in one rank decide
|
|
7348
|
+
* nothing: the answer is null.
|
|
7349
|
+
*/
|
|
7350
|
+
statedInFunction(value, func, reads) {
|
|
7351
|
+
const holders = new Set();
|
|
7352
|
+
const positions = [value];
|
|
7353
|
+
const holderUse = (node) => {
|
|
7354
|
+
if (!Node.isIdentifier(node))
|
|
7355
|
+
return false;
|
|
7356
|
+
const symbol = node.getSymbol()?.compilerSymbol;
|
|
7357
|
+
return !!symbol && holders.has(symbol);
|
|
7288
7358
|
};
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7359
|
+
const hold = (name) => {
|
|
7360
|
+
if (!Node.isIdentifier(name))
|
|
7361
|
+
return;
|
|
7362
|
+
const symbol = name.getSymbol()?.compilerSymbol;
|
|
7363
|
+
if (!symbol || holders.has(symbol))
|
|
7364
|
+
return;
|
|
7365
|
+
holders.add(symbol);
|
|
7366
|
+
for (const id of func.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
7367
|
+
if (id === name || id.getSymbol()?.compilerSymbol !== symbol)
|
|
7368
|
+
continue;
|
|
7369
|
+
const parent = id.getParent();
|
|
7370
|
+
if (Node.isVariableDeclaration(parent) && parent.getNameNode() === id)
|
|
7371
|
+
continue;
|
|
7372
|
+
if (Node.isBinaryExpression(parent) &&
|
|
7373
|
+
parent.getLeft() === id &&
|
|
7374
|
+
parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
|
|
7375
|
+
continue;
|
|
7376
|
+
}
|
|
7377
|
+
positions.push(id);
|
|
7378
|
+
}
|
|
7379
|
+
};
|
|
7380
|
+
hold(value);
|
|
7381
|
+
const found = [];
|
|
7382
|
+
const stateNode = (rank, typeNode, at, how) => {
|
|
7383
|
+
if (!typeNode || this.leavesAPositionOpen(typeNode))
|
|
7384
|
+
return;
|
|
7385
|
+
const text = this.structuralTextFromTypeNode(typeNode);
|
|
7386
|
+
if (text)
|
|
7387
|
+
found.push({ rank, text, at, how });
|
|
7388
|
+
};
|
|
7389
|
+
for (let i = 0; i < positions.length; i++) {
|
|
7390
|
+
const position = positions[i];
|
|
7391
|
+
if (reads.skipFailurePaths &&
|
|
7392
|
+
position !== value &&
|
|
7393
|
+
reachedOnlyOnFailure(position, func, holderUse)) {
|
|
7309
7394
|
continue;
|
|
7310
|
-
const candidates = [];
|
|
7311
|
-
const callee = call.getExpression();
|
|
7312
|
-
if (Node.isPropertyAccessExpression(callee)) {
|
|
7313
|
-
candidates.push(callee.getExpression());
|
|
7314
7395
|
}
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
for (
|
|
7320
|
-
const
|
|
7321
|
-
if (!
|
|
7322
|
-
|
|
7396
|
+
// Climb the wrappers that leave the value what it was: parentheses,
|
|
7397
|
+
// `await`, `!`, `satisfies`, a placeholder cast, and an empty default
|
|
7398
|
+
// (`body ?? {}`).
|
|
7399
|
+
let outer = position;
|
|
7400
|
+
for (;;) {
|
|
7401
|
+
const parent = outer.getParent();
|
|
7402
|
+
if (!parent)
|
|
7403
|
+
break;
|
|
7404
|
+
if ((Node.isParenthesizedExpression(parent) ||
|
|
7405
|
+
Node.isAwaitExpression(parent) ||
|
|
7406
|
+
Node.isNonNullExpression(parent) ||
|
|
7407
|
+
Node.isSatisfiesExpression(parent)) &&
|
|
7408
|
+
parent.getExpression() === outer) {
|
|
7409
|
+
outer = parent;
|
|
7410
|
+
continue;
|
|
7411
|
+
}
|
|
7412
|
+
if ((Node.isAsExpression(parent) || Node.isTypeAssertion(parent)) &&
|
|
7413
|
+
parent.getExpression() === outer) {
|
|
7414
|
+
const typeNode = parent.getTypeNode();
|
|
7415
|
+
if (typeNode && !this.leavesAPositionOpen(typeNode)) {
|
|
7416
|
+
stateNode(3, typeNode, parent, 'a cast of the whole value');
|
|
7417
|
+
break;
|
|
7418
|
+
}
|
|
7419
|
+
outer = parent;
|
|
7323
7420
|
continue;
|
|
7324
7421
|
}
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7422
|
+
if (Node.isBinaryExpression(parent) &&
|
|
7423
|
+
parent.getLeft() === outer &&
|
|
7424
|
+
[SyntaxKind.QuestionQuestionToken, SyntaxKind.BarBarToken].includes(parent.getOperatorToken().getKind()) &&
|
|
7425
|
+
this.isEmptyDefault(parent.getRight())) {
|
|
7426
|
+
outer = parent;
|
|
7427
|
+
continue;
|
|
7428
|
+
}
|
|
7429
|
+
break;
|
|
7430
|
+
}
|
|
7431
|
+
const parent = outer.getParent();
|
|
7432
|
+
if (!parent)
|
|
7433
|
+
continue;
|
|
7434
|
+
if (Node.isVariableDeclaration(parent) && parent.getInitializer() === outer) {
|
|
7435
|
+
stateNode(0, parent.getTypeNode(), parent, 'the annotation of the name that holds it');
|
|
7436
|
+
hold(parent.getNameNode());
|
|
7437
|
+
continue;
|
|
7438
|
+
}
|
|
7439
|
+
if (Node.isBinaryExpression(parent) &&
|
|
7440
|
+
parent.getRight() === outer &&
|
|
7441
|
+
parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken &&
|
|
7442
|
+
Node.isIdentifier(parent.getLeft())) {
|
|
7443
|
+
const declaration = parent.getLeft().getSymbol()?.getValueDeclaration();
|
|
7444
|
+
if (declaration && Node.isVariableDeclaration(declaration)) {
|
|
7445
|
+
stateNode(0, declaration.getTypeNode(), declaration, 'the annotation of the name it is assigned to');
|
|
7446
|
+
}
|
|
7447
|
+
hold(parent.getLeft());
|
|
7448
|
+
continue;
|
|
7449
|
+
}
|
|
7450
|
+
if (reads.returned) {
|
|
7451
|
+
const returnedHere = (Node.isReturnStatement(parent) &&
|
|
7452
|
+
parent.getExpression() === outer &&
|
|
7453
|
+
this.findContainingFunctionForNode(parent) === func) ||
|
|
7454
|
+
(Node.isArrowFunction(parent) && parent === func && parent.getBody() === outer);
|
|
7455
|
+
if (returnedHere) {
|
|
7456
|
+
stateNode(0, func.getReturnTypeNode(), func, 'the declared return type of the function that returns it');
|
|
7457
|
+
continue;
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
if (Node.isCallExpression(parent) && parent.getArguments().includes(outer)) {
|
|
7461
|
+
this.stateAtReceivingCall(parent, parent.getArguments().indexOf(outer), reads, found, stateNode);
|
|
7328
7462
|
}
|
|
7329
7463
|
}
|
|
7330
|
-
|
|
7464
|
+
if (found.length === 0)
|
|
7465
|
+
return null;
|
|
7466
|
+
const rank = Math.min(...found.map((entry) => entry.rank));
|
|
7467
|
+
const decided = found.filter((entry) => entry.rank === rank);
|
|
7468
|
+
if (new Set(decided.map((entry) => entry.text)).size !== 1) {
|
|
7469
|
+
this.log(`A value at ${this.getNodeLocation(value).file_path}:${this.getNodeLocation(value).start_line} ` +
|
|
7470
|
+
`is stated as ${decided.length} different types in its function; stating none`);
|
|
7471
|
+
return null;
|
|
7472
|
+
}
|
|
7473
|
+
const [first] = decided;
|
|
7474
|
+
return { text: first.text, provenance: first.provenance, at: first.at, how: first.how };
|
|
7475
|
+
}
|
|
7476
|
+
/**
|
|
7477
|
+
* What a call that takes the value at argument `index` states for it:
|
|
7478
|
+
* a schema among its receiver and other arguments (rank 1), a callee of the
|
|
7479
|
+
* project that returns that argument unchanged (rank 2), or, with
|
|
7480
|
+
* `reads.parameters`, the parameter's own type (rank 4).
|
|
7481
|
+
*/
|
|
7482
|
+
stateAtReceivingCall(call, index, reads, found, stateNode) {
|
|
7483
|
+
const schemaCandidates = [];
|
|
7484
|
+
const callee = call.getExpression();
|
|
7485
|
+
if (Node.isPropertyAccessExpression(callee))
|
|
7486
|
+
schemaCandidates.push(callee.getExpression());
|
|
7487
|
+
call.getArguments().forEach((argument, at) => {
|
|
7488
|
+
if (at !== index)
|
|
7489
|
+
schemaCandidates.push(this.unwrapExpressionNode(argument));
|
|
7490
|
+
});
|
|
7491
|
+
for (const candidate of schemaCandidates) {
|
|
7492
|
+
const candidateType = candidate.getType();
|
|
7493
|
+
if (!this.schemaOutputType(candidateType, candidate) &&
|
|
7494
|
+
!this.schemaInputType(candidateType, candidate)) {
|
|
7495
|
+
continue;
|
|
7496
|
+
}
|
|
7497
|
+
const contract = this.schemaContract(candidate, 'input');
|
|
7498
|
+
if (contract) {
|
|
7499
|
+
found.push({ rank: 1, text: contract.text, provenance: contract.provenance, at: call, how: 'a schema it is handed to' });
|
|
7500
|
+
return;
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
const passThrough = this.passThroughFunction(call, index);
|
|
7504
|
+
if (passThrough) {
|
|
7505
|
+
stateNode(2, passThrough.getReturnTypeNode(), call, 'a function that returns it unchanged');
|
|
7506
|
+
return;
|
|
7507
|
+
}
|
|
7508
|
+
if (!reads.parameters)
|
|
7509
|
+
return;
|
|
7510
|
+
const signature = this.project.getTypeChecker().getResolvedSignature(call);
|
|
7511
|
+
const parameter = signature?.getParameters()[index];
|
|
7512
|
+
const declaration = parameter?.getValueDeclaration();
|
|
7513
|
+
if (!parameter || (declaration && Node.isParameterDeclaration(declaration) && declaration.isRestParameter())) {
|
|
7514
|
+
return;
|
|
7515
|
+
}
|
|
7516
|
+
let parameterType = parameter.getTypeAtLocation(call);
|
|
7517
|
+
if (parameterType.isUnion()) {
|
|
7518
|
+
const bodies = parameterType.getUnionTypes().filter((part) => !this.isCallableType(part));
|
|
7519
|
+
if (bodies.length !== 1)
|
|
7520
|
+
return;
|
|
7521
|
+
parameterType = bodies[0];
|
|
7522
|
+
}
|
|
7523
|
+
if (!this.isObjectShape(parameterType))
|
|
7524
|
+
return;
|
|
7525
|
+
const text = this.structuralTextFromType(parameterType, call);
|
|
7526
|
+
if (!text || /\b(any|unknown)\b/.test(text))
|
|
7527
|
+
return;
|
|
7528
|
+
found.push({ rank: 4, text, at: call, how: 'the parameter of a call it is handed to' });
|
|
7529
|
+
}
|
|
7530
|
+
/**
|
|
7531
|
+
* The project function `call` invokes, when every value it returns is its
|
|
7532
|
+
* argument at `index` as it was handed in (through parentheses, casts,
|
|
7533
|
+
* `satisfies` and `!`) or an empty default (`{}`, `[]`, `null`,
|
|
7534
|
+
* `undefined`), and at least one return is that argument. Such a function
|
|
7535
|
+
* narrows its argument, so its declared return type describes the value it
|
|
7536
|
+
* was handed. Any other function builds something new from it, and is not
|
|
7537
|
+
* this.
|
|
7538
|
+
*/
|
|
7539
|
+
passThroughFunction(call, index) {
|
|
7540
|
+
let symbol = call.getExpression().getSymbol();
|
|
7541
|
+
if (symbol?.isAlias())
|
|
7542
|
+
symbol = symbol.getAliasedSymbol() ?? symbol;
|
|
7543
|
+
for (const declaration of symbol?.getDeclarations() ?? []) {
|
|
7544
|
+
const fn = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration)
|
|
7545
|
+
? declaration
|
|
7546
|
+
: Node.isVariableDeclaration(declaration)
|
|
7547
|
+
? this.unwrapExpressionNode(declaration.getInitializer())
|
|
7548
|
+
: undefined;
|
|
7549
|
+
if (!fn ||
|
|
7550
|
+
!(Node.isFunctionDeclaration(fn) ||
|
|
7551
|
+
Node.isMethodDeclaration(fn) ||
|
|
7552
|
+
Node.isArrowFunction(fn) ||
|
|
7553
|
+
Node.isFunctionExpression(fn)) ||
|
|
7554
|
+
!fn.getBody() ||
|
|
7555
|
+
fn.getSourceFile().isDeclarationFile() ||
|
|
7556
|
+
fn.getSourceFile().isInNodeModules()) {
|
|
7557
|
+
continue;
|
|
7558
|
+
}
|
|
7559
|
+
const parameter = fn.getParameters()[index];
|
|
7560
|
+
const parameterSymbol = parameter?.getNameNode().getSymbol()?.compilerSymbol;
|
|
7561
|
+
if (!parameter || parameter.isRestParameter() || !parameterSymbol)
|
|
7562
|
+
continue;
|
|
7563
|
+
const returned = this.returnedExpressions(fn);
|
|
7564
|
+
let returnsArgument = false;
|
|
7565
|
+
const everyReturnPasses = returned.every((expression) => {
|
|
7566
|
+
let node = expression;
|
|
7567
|
+
while (Node.isParenthesizedExpression(node) ||
|
|
7568
|
+
Node.isAsExpression(node) ||
|
|
7569
|
+
Node.isTypeAssertion(node) ||
|
|
7570
|
+
Node.isSatisfiesExpression(node) ||
|
|
7571
|
+
Node.isNonNullExpression(node)) {
|
|
7572
|
+
node = node.getExpression();
|
|
7573
|
+
}
|
|
7574
|
+
if (Node.isIdentifier(node) && node.getSymbol()?.compilerSymbol === parameterSymbol) {
|
|
7575
|
+
returnsArgument = true;
|
|
7576
|
+
return true;
|
|
7577
|
+
}
|
|
7578
|
+
return this.isEmptyDefault(node);
|
|
7579
|
+
});
|
|
7580
|
+
if (everyReturnPasses && returnsArgument)
|
|
7581
|
+
return fn;
|
|
7582
|
+
}
|
|
7583
|
+
return undefined;
|
|
7584
|
+
}
|
|
7585
|
+
/** `{}`, `[]`, `null` or `undefined`: a default that states no member. */
|
|
7586
|
+
isEmptyDefault(node) {
|
|
7587
|
+
const bare = this.unwrapExpressionNode(node);
|
|
7588
|
+
return ((Node.isObjectLiteralExpression(bare) && bare.getProperties().length === 0) ||
|
|
7589
|
+
(Node.isArrayLiteralExpression(bare) && bare.getElements().length === 0) ||
|
|
7590
|
+
Node.isNullLiteral(bare) ||
|
|
7591
|
+
(Node.isIdentifier(bare) && bare.getText() === 'undefined'));
|
|
7331
7592
|
}
|
|
7332
7593
|
/**
|
|
7333
7594
|
* The part a validated read names when that part is not a body, or
|
|
@@ -411,6 +411,18 @@ export interface Yields {
|
|
|
411
411
|
export: string;
|
|
412
412
|
side: string;
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Where a route handler's request holds its path and method (#2474 R0): the
|
|
416
|
+
* handler's parameter `param` is the request, and each of the others is a
|
|
417
|
+
* member path read off it, relative to that parameter (`['path']`,
|
|
418
|
+
* `['raw', 'url']`). At least one of `path` or `url`.
|
|
419
|
+
*/
|
|
420
|
+
export interface RequestAt {
|
|
421
|
+
param: number;
|
|
422
|
+
path?: string[];
|
|
423
|
+
url?: string[];
|
|
424
|
+
method?: string[];
|
|
425
|
+
}
|
|
414
426
|
/** The closed role list; the role alone picks the checks a claim needs. */
|
|
415
427
|
export type LibraryRole = 'http_client' | 'graphql_client' | 'broker' | 'in_process_bus' | 'socket' | 'server_framework' | 'none';
|
|
416
428
|
/** What an `op` claim does on the wire. */
|
|
@@ -467,6 +479,15 @@ export type LibraryClaim = {
|
|
|
467
479
|
key_labels?: Record<string, KeyLabel>;
|
|
468
480
|
name_scope?: NameScope;
|
|
469
481
|
picker?: string;
|
|
482
|
+
/**
|
|
483
|
+
* Route fields (#2474 R0): the member runs the function here and hands
|
|
484
|
+
* it, as `yields` says, an instance bound under `name`. Comes with
|
|
485
|
+
* `yields`.
|
|
486
|
+
*/
|
|
487
|
+
runs?: ClaimSlot;
|
|
488
|
+
yields?: Yields;
|
|
489
|
+
/** Route fields (#2474 R0): the instance handed here is served under `name`. */
|
|
490
|
+
mounts?: ClaimSlot;
|
|
470
491
|
} | {
|
|
471
492
|
kind: 'op';
|
|
472
493
|
op: LibraryOp;
|
|
@@ -502,6 +523,8 @@ export type LibraryClaim = {
|
|
|
502
523
|
runs?: RunsSlot;
|
|
503
524
|
/** Label v2, `receive` only: what the handler is handed. */
|
|
504
525
|
yields?: Yields;
|
|
526
|
+
/** Route fields (#2474 R0), `receive` only: where the handler's request holds its path and method. */
|
|
527
|
+
request?: RequestAt;
|
|
505
528
|
} | {
|
|
506
529
|
/** A name the library emits itself, spelled in one of `member`'s parameters. */
|
|
507
530
|
kind: 'reserved';
|
|
@@ -538,6 +561,8 @@ export interface LibraryCheck {
|
|
|
538
561
|
export interface VerifyLibraryClaimsRequest extends BaseRequest {
|
|
539
562
|
action: 'verify_library_claims';
|
|
540
563
|
from_dir: string;
|
|
564
|
+
/** The scanned repo's root, as on `init`: a verdict's `declarations` are named from it, as position rows are. */
|
|
565
|
+
scan_root?: string;
|
|
541
566
|
checks: LibraryCheck[];
|
|
542
567
|
/** Checks not reached in time come back `unchecked` with reason `budget`; null or absent is the default. */
|
|
543
568
|
budget_ms?: number | null;
|
|
@@ -825,6 +850,11 @@ export interface RetypeCheckResponse extends BaseResponse {
|
|
|
825
850
|
outcomes?: RetypeOutcome[];
|
|
826
851
|
errors?: string[];
|
|
827
852
|
}
|
|
853
|
+
/** Where a verified op's member is declared on its receiver, as a position row's `decl_file` and `decl_line`. */
|
|
854
|
+
export interface SemanticsDeclaration {
|
|
855
|
+
decl_file: string;
|
|
856
|
+
decl_line: number;
|
|
857
|
+
}
|
|
828
858
|
/**
|
|
829
859
|
* The verdict on one `(claim_id, receiver)` pair.
|
|
830
860
|
*
|
|
@@ -838,6 +868,8 @@ export interface SemanticsResult {
|
|
|
838
868
|
verdict: 'verified' | 'failed' | 'unchecked';
|
|
839
869
|
/** Absent exactly when verified. */
|
|
840
870
|
reason?: string;
|
|
871
|
+
/** Verified op verdicts only: the declarations its member resolves to on the receiver. */
|
|
872
|
+
declarations?: SemanticsDeclaration[];
|
|
841
873
|
}
|
|
842
874
|
/** How one package of the request resolved, for logs. */
|
|
843
875
|
export interface SemanticsModule {
|