carrick 0.3.84 → 0.3.86
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/README.md +15 -0
- package/bin/carrick.mjs +89 -0
- package/dist/auth/credentials.d.ts +9 -0
- package/dist/auth/credentials.js +13 -2
- package/dist/auth/credentials.js.map +1 -1
- package/dist/auth/read.d.ts +4 -4
- package/dist/global-install.d.ts +183 -0
- package/dist/global-install.js +393 -0
- package/dist/global-install.js.map +1 -0
- package/dist/hook/post-edit.js +10 -0
- package/dist/hook/post-edit.js.map +1 -1
- package/dist/hook/session-start.js +34 -0
- package/dist/hook/session-start.js.map +1 -1
- package/dist/hook/stop.js +18 -4
- package/dist/hook/stop.js.map +1 -1
- package/dist/hook/user-prompt.js +16 -4
- package/dist/hook/user-prompt.js.map +1 -1
- package/dist/init/doctor.d.ts +40 -0
- package/dist/init/doctor.js +125 -0
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/outdated.d.ts +47 -0
- package/dist/init/outdated.js +104 -0
- package/dist/init/outdated.js.map +1 -1
- package/dist/init/projects.d.ts +2 -2
- package/dist/init/run.d.ts +9 -0
- package/dist/init/run.js +57 -12
- package/dist/init/run.js.map +1 -1
- package/dist/scan.d.ts +26 -0
- package/dist/scan.js +92 -0
- package/dist/scan.js.map +1 -1
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +25 -0
- package/dist/update-check.js.map +1 -0
- package/dist/update.d.ts +128 -0
- package/dist/update.js +398 -0
- package/dist/update.js.map +1 -0
- package/package.json +7 -7
- package/sidecar/dist/src/capture/anchors.js +69 -8
- package/sidecar/dist/src/capture/api.d.ts +4 -1
- package/sidecar/dist/src/capture/deep-walk.js +4 -1
- package/sidecar/dist/src/capture/node-builder.d.ts +28 -1
- package/sidecar/dist/src/capture/node-builder.js +89 -3
- package/sidecar/dist/src/capture/unresolved.d.ts +7 -0
- package/sidecar/dist/src/capture/unresolved.js +1 -1
- package/sidecar/dist/src/type-inferrer.d.ts +128 -5
- package/sidecar/dist/src/type-inferrer.js +444 -17
- package/templates/skills/carrick-census.md +3 -1
|
@@ -865,7 +865,7 @@ export class TypeInferrer {
|
|
|
865
865
|
const serialisers = this.calleesProvenSerialiser(branches);
|
|
866
866
|
const isSend = (candidate) => this.isResponseSend(candidate, extractionConfig, serialisers);
|
|
867
867
|
const peeled = this.peelTransparentExpression(located);
|
|
868
|
-
const site = isSend(peeled) ? peeled : this.
|
|
868
|
+
const site = isSend(peeled) ? peeled : this.receivingCallOf(located, isSend);
|
|
869
869
|
if (!site)
|
|
870
870
|
return undefined;
|
|
871
871
|
const contains = (outer, inner) => outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
|
|
@@ -938,12 +938,16 @@ export class TypeInferrer {
|
|
|
938
938
|
this.typeIsOrContainsResponseMachinery(result));
|
|
939
939
|
}
|
|
940
940
|
/**
|
|
941
|
-
* The
|
|
941
|
+
* The call `node` is an ARGUMENT of, looking through the wrappers that do
|
|
942
942
|
* not change a payload (parentheses, `as`, `satisfies`, `!`, `await`) and a
|
|
943
|
-
* `JSON.stringify` around the body. `undefined` when
|
|
944
|
-
*
|
|
943
|
+
* `JSON.stringify` around the body. `undefined` when `accept` rejects that
|
|
944
|
+
* call or `node` is its callee.
|
|
945
|
+
*
|
|
946
|
+
* Two readings use it: a value handed to a response send is the payload that
|
|
947
|
+
* send transmits, and a body read handed to a call that states what it
|
|
948
|
+
* returns is a better statement of that body than the read (carrick#1382).
|
|
945
949
|
*/
|
|
946
|
-
|
|
950
|
+
receivingCallOf(node, accept) {
|
|
947
951
|
let current = node;
|
|
948
952
|
let parent = current.getParent();
|
|
949
953
|
while (parent &&
|
|
@@ -961,7 +965,7 @@ export class TypeInferrer {
|
|
|
961
965
|
}
|
|
962
966
|
if (!parent.getArguments().includes(current))
|
|
963
967
|
return undefined;
|
|
964
|
-
return
|
|
968
|
+
return accept(parent) ? parent : undefined;
|
|
965
969
|
}
|
|
966
970
|
inferCallResult(sourceFile, request, extractionConfig) {
|
|
967
971
|
const callExpr = this.resolveTargetCallExpression(sourceFile, request);
|
|
@@ -970,7 +974,8 @@ export class TypeInferrer {
|
|
|
970
974
|
}
|
|
971
975
|
// Walk up from the already-found call expression instead of re-searching
|
|
972
976
|
const func = this.findContainingFunctionForNode(callExpr);
|
|
973
|
-
const
|
|
977
|
+
const use = this.resolveCallResultTerminalNode(callExpr, func);
|
|
978
|
+
const terminalNode = use.terminal;
|
|
974
979
|
const returnType = terminalNode.getType();
|
|
975
980
|
let typeString = typeText(returnType, terminalNode);
|
|
976
981
|
let isExplicit = false;
|
|
@@ -984,6 +989,76 @@ export class TypeInferrer {
|
|
|
984
989
|
typeString = explicitType;
|
|
985
990
|
isExplicit = true;
|
|
986
991
|
}
|
|
992
|
+
// carrick#1376: the call answers a RESULT CARRIER — a generic union whose
|
|
993
|
+
// branches say whether the call worked and carry, on the success side, the
|
|
994
|
+
// value it produced. The carrier is the transport's own bookkeeping; the
|
|
995
|
+
// payload a caller receives is the success type argument, and publishing
|
|
996
|
+
// the carrier instead reports an envelope as a wire contract, which is the
|
|
997
|
+
// same class of answer the machinery guard refuses on the producer side.
|
|
998
|
+
//
|
|
999
|
+
// A wrapper rule that already unwrapped, and a type the source itself
|
|
1000
|
+
// states, both outrank this: they are what the service's own config and
|
|
1001
|
+
// its own author said.
|
|
1002
|
+
const carrierCandidate = unwrapResult.wasUnwrapped || explicitType
|
|
1003
|
+
? undefined
|
|
1004
|
+
: this.resultCarrierPayload(returnType, terminalNode, use.projections, `${request.file_path}:${request.line_number}`);
|
|
1005
|
+
// The payload rides the row as its own MEMBERS, never as its bare name
|
|
1006
|
+
// (#257): `derive_capture_anchors` turns a usable inference into a literal
|
|
1007
|
+
// capture anchor, and a bare name is out of scope where the surface
|
|
1008
|
+
// declares the alias, so it would decay to a top type and publish nothing
|
|
1009
|
+
// — trading a wrong answer for no answer. Where the payload carries no
|
|
1010
|
+
// member shape to print, the carrier keeps its own answer.
|
|
1011
|
+
const carrierText = carrierCandidate
|
|
1012
|
+
? this.structuralTextFromType(carrierCandidate, terminalNode)
|
|
1013
|
+
: null;
|
|
1014
|
+
const carrierPayload = carrierText ? carrierCandidate : undefined;
|
|
1015
|
+
if (carrierPayload && carrierText) {
|
|
1016
|
+
typeString = carrierText;
|
|
1017
|
+
}
|
|
1018
|
+
else if (carrierCandidate) {
|
|
1019
|
+
this.log(`Call result at ${request.file_path}:${request.line_number} carries a payload with no ` +
|
|
1020
|
+
'printable member shape; publishing the carrier as written');
|
|
1021
|
+
}
|
|
1022
|
+
// carrick#1375: the source never read this call's result whole — every
|
|
1023
|
+
// read took a member out of it, and one of those members is the generic
|
|
1024
|
+
// this result was instantiated with. That is a source unwrapping an
|
|
1025
|
+
// envelope by hand, so both answers available here are wrong: the
|
|
1026
|
+
// projection is a fragment of the payload, and the envelope around it is
|
|
1027
|
+
// machinery. A wrapper rule that DID unwrap the envelope, or a type the
|
|
1028
|
+
// source itself states, is a contract and keeps its answer.
|
|
1029
|
+
//
|
|
1030
|
+
// A single explicit call generic (`client.get<Order[]>(url)`) is the
|
|
1031
|
+
// caller's own payload claim and states the contract as plainly as an
|
|
1032
|
+
// `as` does — it is what the anchor below falls back to on a checkout
|
|
1033
|
+
// with no node_modules. Several generics claim nothing unambiguously.
|
|
1034
|
+
//
|
|
1035
|
+
// The decision has to ride the row: a plain `null` is re-read by the
|
|
1036
|
+
// capture's own locator, which is kind-blind and would publish the
|
|
1037
|
+
// projection this just declined to publish (`inference_decided_no_contract`,
|
|
1038
|
+
// engine/type_compat_v2.rs). No anchor and no depth go with it, so the
|
|
1039
|
+
// row also reads as blind rather than as a sighted answer.
|
|
1040
|
+
if (use.projectionOnly &&
|
|
1041
|
+
this.projectionReadsGenericPayload(callExpr, use.projections) &&
|
|
1042
|
+
!unwrapResult.wasUnwrapped &&
|
|
1043
|
+
!explicitType &&
|
|
1044
|
+
!carrierPayload &&
|
|
1045
|
+
callExpr.getTypeArguments().length !== 1) {
|
|
1046
|
+
this.log(`Call result at ${request.file_path}:${request.line_number} is only ever read as ` +
|
|
1047
|
+
`members of ${typeText(callExpr.getType(), callExpr)}; a part of a payload is not ` +
|
|
1048
|
+
'a payload, so this site states no response contract');
|
|
1049
|
+
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
1050
|
+
abstain.any_provenance = [
|
|
1051
|
+
{
|
|
1052
|
+
path: '',
|
|
1053
|
+
kind: 'unknown',
|
|
1054
|
+
reason: 'projected_value_only',
|
|
1055
|
+
detail: "every read of this call's result takes a member out of it and none reads the " +
|
|
1056
|
+
'value itself, so what this site states is a part of a payload rather than the ' +
|
|
1057
|
+
'payload a caller receives',
|
|
1058
|
+
},
|
|
1059
|
+
];
|
|
1060
|
+
return abstain;
|
|
1061
|
+
}
|
|
987
1062
|
typeString = this.unwrapPromise(typeString, returnType);
|
|
988
1063
|
// #336: consumer analogue of the #306 producer anchor. Anchor on the
|
|
989
1064
|
// CALL's own payload (the rule-extracted Type, else the awaited call
|
|
@@ -1001,9 +1076,17 @@ export class TypeInferrer {
|
|
|
1001
1076
|
// anchor via that path.
|
|
1002
1077
|
const callPayloadType = this.unwrapPromiseType(callExpr.getType());
|
|
1003
1078
|
const callUnwrap = this.unwrapTypeWithConfig(callPayloadType, callExpr, extractionConfig);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1079
|
+
// carrick#1376: the carrier's own symbol must never anchor either. The
|
|
1080
|
+
// surface pre-claims the alias from the anchor, so anchoring on the
|
|
1081
|
+
// carrier makes the capture emit the transport's bookkeeping as the
|
|
1082
|
+
// operation's declaration — and where the carrier is a local interface the
|
|
1083
|
+
// service does not export, nothing is emitted at all and the row reads
|
|
1084
|
+
// null. The payload the carrier was found to hold is the anchor.
|
|
1085
|
+
const anchorSource = carrierPayload
|
|
1086
|
+
? carrierPayload
|
|
1087
|
+
: callUnwrap.wasUnwrapped
|
|
1088
|
+
? callUnwrap.payloadType
|
|
1089
|
+
: callPayloadType;
|
|
1007
1090
|
let anchor = anchorSource
|
|
1008
1091
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
1009
1092
|
: undefined;
|
|
@@ -1314,13 +1397,17 @@ export class TypeInferrer {
|
|
|
1314
1397
|
if (returnStmt) {
|
|
1315
1398
|
const returnExpr = returnStmt.getExpression();
|
|
1316
1399
|
if (returnExpr) {
|
|
1317
|
-
return returnExpr;
|
|
1400
|
+
return { terminal: returnExpr, projectionOnly: false, projections: [] };
|
|
1318
1401
|
}
|
|
1319
1402
|
}
|
|
1320
1403
|
const binding = this.extractBindingFromCall(callExpr);
|
|
1321
1404
|
if (binding && func) {
|
|
1322
1405
|
let currentNames = binding.names;
|
|
1323
1406
|
let lastNode = binding.node;
|
|
1407
|
+
/** The member reads taken OUT of the tracked value (carrick#1375). */
|
|
1408
|
+
const projections = [];
|
|
1409
|
+
/** The value itself was read: returned, passed, aliased, or body-read. */
|
|
1410
|
+
let sawWholeRead = false;
|
|
1324
1411
|
const startPos = callExpr.getStart();
|
|
1325
1412
|
const candidates = this.collectDefUseNodes(func);
|
|
1326
1413
|
for (const expr of candidates) {
|
|
@@ -1344,9 +1431,27 @@ export class TypeInferrer {
|
|
|
1344
1431
|
if (Node.isIdentifier(expr) && this.isIdentifierUsage(expr, currentNames)) {
|
|
1345
1432
|
const bodyRead = this.bodyReadOnReceiver(expr);
|
|
1346
1433
|
if (bodyRead) {
|
|
1347
|
-
|
|
1434
|
+
sawWholeRead = true;
|
|
1435
|
+
// carrick#1382: the read is a FLOOR, not a ceiling. Where the
|
|
1436
|
+
// source hands the body it read to a call that states what it
|
|
1437
|
+
// returns, that call says strictly more about the payload than
|
|
1438
|
+
// the untyped read does, and it must not lose to the read just
|
|
1439
|
+
// because the walk reaches the read second (candidates come in
|
|
1440
|
+
// pre-order, so the declaration of the parsed value is visited
|
|
1441
|
+
// before the identifier inside its own initializer).
|
|
1442
|
+
lastNode = this.statedPayloadAroundBodyRead(bodyRead) ?? bodyRead;
|
|
1348
1443
|
continue;
|
|
1349
1444
|
}
|
|
1445
|
+
// carrick#1375: how the source reads the value decides whether this
|
|
1446
|
+
// site states a payload at all. A member read takes a PART of it; a
|
|
1447
|
+
// return, an argument, an alias or a parse reads the value itself.
|
|
1448
|
+
const projection = this.projectionOnReceiver(expr);
|
|
1449
|
+
if (projection) {
|
|
1450
|
+
projections.push(projection);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
sawWholeRead = true;
|
|
1454
|
+
}
|
|
1350
1455
|
}
|
|
1351
1456
|
if (Node.isVariableDeclaration(expr)) {
|
|
1352
1457
|
const initializer = expr.getInitializer();
|
|
@@ -1370,13 +1475,326 @@ export class TypeInferrer {
|
|
|
1370
1475
|
}
|
|
1371
1476
|
}
|
|
1372
1477
|
}
|
|
1373
|
-
|
|
1478
|
+
// The last expression the value flows into is the call's payload — a
|
|
1479
|
+
// cast, a parse, an alias. An expression that only reads MEMBERS out
|
|
1480
|
+
// of it is a projection and states a part of the payload, never the
|
|
1481
|
+
// payload: the hook that derives `query.data?.flags` off a query
|
|
1482
|
+
// result published a boolean map as the expected response of a call
|
|
1483
|
+
// whose envelope the client declares in full (carrick#1375).
|
|
1484
|
+
if (this.expressionUsesNames(expr, currentNames) &&
|
|
1485
|
+
!this.usesNamesOnlyByProjection(expr, currentNames)) {
|
|
1374
1486
|
lastNode = expr;
|
|
1375
1487
|
}
|
|
1376
1488
|
}
|
|
1377
|
-
return
|
|
1489
|
+
return {
|
|
1490
|
+
terminal: lastNode,
|
|
1491
|
+
projectionOnly: projections.length > 0 && !sawWholeRead,
|
|
1492
|
+
projections,
|
|
1493
|
+
};
|
|
1378
1494
|
}
|
|
1379
|
-
return callExpr;
|
|
1495
|
+
return { terminal: callExpr, projectionOnly: false, projections: [] };
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* True when a member read on the call's result resolves to one of that
|
|
1499
|
+
* result's own TYPE ARGUMENTS — the source is unwrapping a generic envelope
|
|
1500
|
+
* by hand (`state.data` off a `ResourceState<Envelope>`), and the payload it
|
|
1501
|
+
* carries is the instantiation, not the envelope (carrick#1375).
|
|
1502
|
+
*
|
|
1503
|
+
* The generic is what tells the two apart. A call that answers its payload
|
|
1504
|
+
* directly is read member by member too, and its declared result IS the
|
|
1505
|
+
* contract; abstaining there would throw away the type the request boundary
|
|
1506
|
+
* states, which a replay over a real repo's consumer rows showed on a
|
|
1507
|
+
* `{ ok: true } | { ok: false; reason: string }` result read as `sent.ok`.
|
|
1508
|
+
*/
|
|
1509
|
+
projectionReadsGenericPayload(callExpr, projections) {
|
|
1510
|
+
if (projections.length === 0)
|
|
1511
|
+
return false;
|
|
1512
|
+
const result = this.unwrapPromiseType(callExpr.getType());
|
|
1513
|
+
const args = [...result.getTypeArguments(), ...result.getAliasTypeArguments()];
|
|
1514
|
+
if (args.length === 0)
|
|
1515
|
+
return false;
|
|
1516
|
+
const argTexts = new Set(args.map((arg) => arg.getText()));
|
|
1517
|
+
return projections.some((projection) => {
|
|
1518
|
+
const read = this.unwrapPromiseType(projection.getType());
|
|
1519
|
+
const parts = [read, read.getNonNullableType()];
|
|
1520
|
+
for (const part of [...parts]) {
|
|
1521
|
+
if (part.isUnion())
|
|
1522
|
+
parts.push(...part.getUnionTypes());
|
|
1523
|
+
}
|
|
1524
|
+
return parts.some((part) => argTexts.has(part.getText()));
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* The payload a RESULT CARRIER carries, or `undefined` when `type` is not
|
|
1529
|
+
* one or its success side cannot be told from its failure side
|
|
1530
|
+
* (carrick#1376).
|
|
1531
|
+
*
|
|
1532
|
+
* A carrier is recognised by its shape, never by a name: a union of object
|
|
1533
|
+
* branches, instantiated with two or more type arguments, at least one of
|
|
1534
|
+
* which a branch holds as a member. `Result<T, E>`, `Either<L, R>` and a
|
|
1535
|
+
* hand-rolled `{ ok: true; value: T } | { ok: false; error: E }` are all the
|
|
1536
|
+
* same shape, and a promise-like around one is peeled first through the
|
|
1537
|
+
* language's own await protocol. A single generic object — a resource state,
|
|
1538
|
+
* a query result — is NOT a union and is left to carrick#1375, which
|
|
1539
|
+
* abstains on it so a sibling site can answer.
|
|
1540
|
+
*
|
|
1541
|
+
* Which argument is the payload is decided twice over, and never guessed:
|
|
1542
|
+
*
|
|
1543
|
+
* 1. the platform's error shape. Exactly one argument that is not
|
|
1544
|
+
* error-shaped, beside at least one that is, is the success side.
|
|
1545
|
+
* 2. what the source reads. Where every argument looks alike — `Pair<A,
|
|
1546
|
+
* string>` — a member read of the carrier that resolves to exactly one
|
|
1547
|
+
* of the arguments names the side this call site takes.
|
|
1548
|
+
*
|
|
1549
|
+
* Where neither decides, the carrier keeps its own answer and the limit is
|
|
1550
|
+
* logged: a coin flip published as a contract is worse than an envelope a
|
|
1551
|
+
* reader can see is an envelope.
|
|
1552
|
+
*/
|
|
1553
|
+
resultCarrierPayload(type, at, projections, where) {
|
|
1554
|
+
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
1555
|
+
if (!carrier.isUnion()) {
|
|
1556
|
+
return undefined;
|
|
1557
|
+
}
|
|
1558
|
+
const branches = carrier.getUnionTypes();
|
|
1559
|
+
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
1560
|
+
return undefined;
|
|
1561
|
+
}
|
|
1562
|
+
const args = [
|
|
1563
|
+
...carrier.getAliasTypeArguments(),
|
|
1564
|
+
...carrier.getTypeArguments(),
|
|
1565
|
+
];
|
|
1566
|
+
if (args.length < 2) {
|
|
1567
|
+
return undefined;
|
|
1568
|
+
}
|
|
1569
|
+
// A type argument only names a payload when a branch actually holds it:
|
|
1570
|
+
// a generic that parameterises a status code or a key carries nothing.
|
|
1571
|
+
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
1572
|
+
.getProperties()
|
|
1573
|
+
.some((property) => {
|
|
1574
|
+
try {
|
|
1575
|
+
return property.getTypeAtLocation(at).getText() === arg.getText();
|
|
1576
|
+
}
|
|
1577
|
+
catch {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
})));
|
|
1581
|
+
if (carried.length === 0) {
|
|
1582
|
+
return undefined;
|
|
1583
|
+
}
|
|
1584
|
+
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
1585
|
+
const read = new Set();
|
|
1586
|
+
for (const projection of projections) {
|
|
1587
|
+
const projected = this.unwrapPromiseType(projection.getType()).getNonNullableType();
|
|
1588
|
+
if (argTexts.has(projected.getText())) {
|
|
1589
|
+
read.add(projected.getText());
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const byRead = read.size === 1 ? argTexts.get([...read][0]) : undefined;
|
|
1593
|
+
const succeeded = carried.filter((arg) => !this.isErrorShaped(arg, at));
|
|
1594
|
+
const byShape = succeeded.length === 1 && succeeded.length < carried.length ? succeeded[0] : undefined;
|
|
1595
|
+
// The two tests must agree where both answer. An error shape is a shape,
|
|
1596
|
+
// and a payload is free to have one — a contact form declares a `name` and
|
|
1597
|
+
// a `message` too, and a service whose failure side is `{ code: number }`
|
|
1598
|
+
// would then read as the success side. Where the source reads a DIFFERENT
|
|
1599
|
+
// argument out than the shape test picked, the two disagree about which
|
|
1600
|
+
// side is which and nothing here knows better than the source does.
|
|
1601
|
+
if (byShape && byRead && byShape !== byRead) {
|
|
1602
|
+
this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose error ` +
|
|
1603
|
+
`shape names '${typeText(byShape, at)}' as the payload while the source reads ` +
|
|
1604
|
+
`'${typeText(byRead, at)}' out of it. Publishing the carrier as written rather ` +
|
|
1605
|
+
'than picking one');
|
|
1606
|
+
return undefined;
|
|
1607
|
+
}
|
|
1608
|
+
if (byShape ?? byRead) {
|
|
1609
|
+
return byShape ?? byRead;
|
|
1610
|
+
}
|
|
1611
|
+
this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}) whose success ` +
|
|
1612
|
+
'side cannot be told from its failure side: no single argument is the only ' +
|
|
1613
|
+
'non-error one and the source reads none of them out. Publishing the carrier as ' +
|
|
1614
|
+
'written rather than guessing which argument is the payload');
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* `Future<T>` -> `T` for a promise-like of the source's own making, read off
|
|
1619
|
+
* the await protocol rather than a name: a `then` whose first parameter is a
|
|
1620
|
+
* callback, whose own first parameter is the value awaiting it yields.
|
|
1621
|
+
* `Promise` and `PromiseLike` are peeled by `unwrapPromiseType` before this.
|
|
1622
|
+
*/
|
|
1623
|
+
unwrapThenableType(type) {
|
|
1624
|
+
let current = type;
|
|
1625
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
1626
|
+
const then = current.getProperty('then');
|
|
1627
|
+
const declaration = then?.getDeclarations()[0];
|
|
1628
|
+
if (!then || !declaration)
|
|
1629
|
+
return current;
|
|
1630
|
+
let onValue;
|
|
1631
|
+
try {
|
|
1632
|
+
onValue = then
|
|
1633
|
+
.getTypeAtLocation(declaration)
|
|
1634
|
+
.getCallSignatures()[0]
|
|
1635
|
+
?.getParameters()[0]
|
|
1636
|
+
?.getTypeAtLocation(declaration);
|
|
1637
|
+
}
|
|
1638
|
+
catch {
|
|
1639
|
+
return current;
|
|
1640
|
+
}
|
|
1641
|
+
const value = onValue
|
|
1642
|
+
?.getCallSignatures()[0]
|
|
1643
|
+
?.getParameters()[0]
|
|
1644
|
+
?.getTypeAtLocation(declaration);
|
|
1645
|
+
if (!value || value === current)
|
|
1646
|
+
return current;
|
|
1647
|
+
current = value;
|
|
1648
|
+
}
|
|
1649
|
+
return current;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* The platform's error shape, in full: `name` and `message` strings AND a
|
|
1653
|
+
* `stack`, which is what the `Error` interface declares and every subclass
|
|
1654
|
+
* of it inherits.
|
|
1655
|
+
*
|
|
1656
|
+
* `stack` is what makes the test a test. A name and a message alone are a
|
|
1657
|
+
* shape a PAYLOAD can have — a contact form declares both — and reading such
|
|
1658
|
+
* a payload as the failure side would publish the other argument, which is
|
|
1659
|
+
* the concrete-but-wrong answer this whole rule exists to avoid. A union is
|
|
1660
|
+
* error-shaped when every member of it is.
|
|
1661
|
+
*/
|
|
1662
|
+
isErrorShaped(type, at) {
|
|
1663
|
+
if (type.isUnion()) {
|
|
1664
|
+
return type.getUnionTypes().every((part) => this.isErrorShaped(part, at));
|
|
1665
|
+
}
|
|
1666
|
+
const typeOf = (property) => {
|
|
1667
|
+
if (!property)
|
|
1668
|
+
return undefined;
|
|
1669
|
+
try {
|
|
1670
|
+
return property.getTypeAtLocation(at);
|
|
1671
|
+
}
|
|
1672
|
+
catch {
|
|
1673
|
+
return undefined;
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
const stringy = (property) => typeOf(property)?.getNonNullableType().getText() === 'string';
|
|
1677
|
+
return (stringy(type.getProperty('name')) &&
|
|
1678
|
+
stringy(type.getProperty('message')) &&
|
|
1679
|
+
stringy(type.getProperty('stack')));
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* The member read that takes `identifier` as its RECEIVER — `query` in
|
|
1683
|
+
* `query.data`, `envelope` in `envelope.list[0]` — or `undefined` when the
|
|
1684
|
+
* identifier names the value itself.
|
|
1685
|
+
*
|
|
1686
|
+
* A member CALL is not a projection: `res.text()` yields a body rather than
|
|
1687
|
+
* a part of one, and what it returns stays the walk's business. The
|
|
1688
|
+
* zero-argument json body read has its own branch and is taken before this
|
|
1689
|
+
* is asked.
|
|
1690
|
+
*/
|
|
1691
|
+
projectionOnReceiver(identifier) {
|
|
1692
|
+
const access = identifier.getParent();
|
|
1693
|
+
if (!access ||
|
|
1694
|
+
(!Node.isPropertyAccessExpression(access) &&
|
|
1695
|
+
!Node.isElementAccessExpression(access)) ||
|
|
1696
|
+
access.getExpression() !== identifier) {
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}
|
|
1699
|
+
const parent = access.getParent();
|
|
1700
|
+
if (parent &&
|
|
1701
|
+
Node.isCallExpression(parent) &&
|
|
1702
|
+
parent.getExpression() === access) {
|
|
1703
|
+
return undefined;
|
|
1704
|
+
}
|
|
1705
|
+
return access;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The call that CONSUMES this json body read and states what the body is —
|
|
1709
|
+
* `parseEnvelope(await response.json())` — or `undefined` when nothing
|
|
1710
|
+
* downstream of the read says more about it than the read itself does
|
|
1711
|
+
* (carrick#1382).
|
|
1712
|
+
*
|
|
1713
|
+
* Three conditions, all shapes of the language rather than names:
|
|
1714
|
+
*
|
|
1715
|
+
* - the read reaches the call as an ARGUMENT, through the wrappers that do
|
|
1716
|
+
* not change a value (`await`, parentheses, `as`, `satisfies`, `!`). A
|
|
1717
|
+
* cast with no call around it therefore keeps the read as the terminal,
|
|
1718
|
+
* so `(await res.json()) as Entry` is still read off the read itself;
|
|
1719
|
+
* - the call's own result is BOUND — declared into a variable, returned, or
|
|
1720
|
+
* assigned — so a call the source made for its side effect
|
|
1721
|
+
* (`store(await res.json())`) states nothing about the payload;
|
|
1722
|
+
* - that result is an OBJECT shape. A validator answering `boolean` or a
|
|
1723
|
+
* serialiser answering `string` describes what the caller did with the
|
|
1724
|
+
* body, not what the body is, and publishing it would be a
|
|
1725
|
+
* concrete-but-wrong contract where the honest `any` of the read is
|
|
1726
|
+
* merely unresolved.
|
|
1727
|
+
*/
|
|
1728
|
+
statedPayloadAroundBodyRead(bodyRead) {
|
|
1729
|
+
return this.receivingCallOf(bodyRead, (candidate) => Node.isCallExpression(candidate) &&
|
|
1730
|
+
this.callResultIsBound(candidate) &&
|
|
1731
|
+
this.isObjectShape(this.unwrapPromiseType(candidate.getType())));
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* The source keeps this call's result: it initializes a declaration, is
|
|
1735
|
+
* returned, is assigned, or is an arrow's expression body. A result that is
|
|
1736
|
+
* kept is one the source has a use for; a discarded one is a side effect.
|
|
1737
|
+
*/
|
|
1738
|
+
callResultIsBound(call) {
|
|
1739
|
+
let current = call;
|
|
1740
|
+
let parent = current.getParent();
|
|
1741
|
+
while (parent &&
|
|
1742
|
+
(Node.isParenthesizedExpression(parent) ||
|
|
1743
|
+
Node.isAwaitExpression(parent) ||
|
|
1744
|
+
Node.isAsExpression(parent) ||
|
|
1745
|
+
Node.isSatisfiesExpression(parent) ||
|
|
1746
|
+
Node.isNonNullExpression(parent)) &&
|
|
1747
|
+
parent.getExpression() === current) {
|
|
1748
|
+
current = parent;
|
|
1749
|
+
parent = current.getParent();
|
|
1750
|
+
}
|
|
1751
|
+
if (!parent)
|
|
1752
|
+
return false;
|
|
1753
|
+
if (Node.isVariableDeclaration(parent)) {
|
|
1754
|
+
return parent.getInitializer() === current;
|
|
1755
|
+
}
|
|
1756
|
+
if (Node.isReturnStatement(parent)) {
|
|
1757
|
+
return parent.getExpression() === current;
|
|
1758
|
+
}
|
|
1759
|
+
if (Node.isArrowFunction(parent)) {
|
|
1760
|
+
return parent.getBody() === current;
|
|
1761
|
+
}
|
|
1762
|
+
if (Node.isBinaryExpression(parent)) {
|
|
1763
|
+
return (parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken &&
|
|
1764
|
+
parent.getRight() === current);
|
|
1765
|
+
}
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* A shape a JSON body can be: an object, an array, or a union of them.
|
|
1770
|
+
* Top types, primitives, `void` and callables are not.
|
|
1771
|
+
*/
|
|
1772
|
+
isObjectShape(type) {
|
|
1773
|
+
if (type.isAny() || type.isUnknown())
|
|
1774
|
+
return false;
|
|
1775
|
+
if (type.isUnion()) {
|
|
1776
|
+
const parts = type.getUnionTypes().filter((part) => !part.isUndefined() && !part.isNull());
|
|
1777
|
+
return parts.length > 0 && parts.every((part) => this.isObjectShape(part));
|
|
1778
|
+
}
|
|
1779
|
+
if (type.isIntersection()) {
|
|
1780
|
+
return type.getIntersectionTypes().every((part) => this.isObjectShape(part));
|
|
1781
|
+
}
|
|
1782
|
+
return type.isObject() && !this.isCallableType(type);
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Every use of a tracked name inside `expr` reads a member out of the
|
|
1786
|
+
* tracked value, so the expression's type describes a PART of the payload.
|
|
1787
|
+
* False when the expression uses no tracked name at all, so a caller can
|
|
1788
|
+
* read it as "this is a projection" rather than "this is not a use".
|
|
1789
|
+
*/
|
|
1790
|
+
usesNamesOnlyByProjection(expr, names) {
|
|
1791
|
+
const uses = expr
|
|
1792
|
+
.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
1793
|
+
.filter((id) => this.isIdentifierUsage(id, names));
|
|
1794
|
+
if (uses.length === 0) {
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
return uses.every((id) => this.projectionOnReceiver(id) !== undefined);
|
|
1380
1798
|
}
|
|
1381
1799
|
extractBindingFromCall(callExpr) {
|
|
1382
1800
|
const varDecl = callExpr.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
@@ -2611,7 +3029,16 @@ export class TypeInferrer {
|
|
|
2611
3029
|
* True when an object literal argument is response INIT rather than a body:
|
|
2612
3030
|
* every property it declares is one the standard `ResponseInit` declares
|
|
2613
3031
|
* (`status`, `statusText`, `headers`), and it states at least one of them as
|
|
2614
|
-
* init really does — a
|
|
3032
|
+
* init really does — a status in the HTTP range, or headers.
|
|
3033
|
+
*
|
|
3034
|
+
* The status does NOT have to be a literal code. A route that carries its
|
|
3035
|
+
* outcome in a value writes `new Response(body, { status: result.status })`,
|
|
3036
|
+
* where the source fixes no code and `statedStatusCodes` answers
|
|
3037
|
+
* `'variable'` — status-shaped, just not pinned. Requiring a literal there
|
|
3038
|
+
* left the whole init object reading as a body, so an endpoint whose payload
|
|
3039
|
+
* this layer does not publish (a string body) published `{ status: number }`
|
|
3040
|
+
* as its response contract instead: a wrong contract, served, where an
|
|
3041
|
+
* abstention was the honest answer.
|
|
2615
3042
|
*
|
|
2616
3043
|
* A payload that merely has a `status` member of its own (`{ status: "ok",
|
|
2617
3044
|
* service: "ledger" }`) declares members init does not, or states `status` as
|
|
@@ -2640,7 +3067,7 @@ export class TypeInferrer {
|
|
|
2640
3067
|
statesInit = true;
|
|
2641
3068
|
continue;
|
|
2642
3069
|
}
|
|
2643
|
-
if (name === 'status' &&
|
|
3070
|
+
if (name === 'status' && this.statedStatusCodes(value) !== undefined) {
|
|
2644
3071
|
statesInit = true;
|
|
2645
3072
|
}
|
|
2646
3073
|
}
|
|
@@ -54,7 +54,9 @@ Report these numbers before the list, per query where the field is per query:
|
|
|
54
54
|
removed, and `best` where it names the closest of them. A count above zero is
|
|
55
55
|
the case for one more search at a lower `similarity_threshold`;
|
|
56
56
|
- `total_without_intent`, which is index-wide: functions carrying no intent
|
|
57
|
-
text, which
|
|
57
|
+
text, which the search ranked on their name, signature and body tokens
|
|
58
|
+
alone, so a query that names one finds it and a query that describes what
|
|
59
|
+
it does without naming it does not;
|
|
58
60
|
- `total_intent_carried_forward` where the response states it, which are intents
|
|
59
61
|
describing the code as of an earlier scan.
|
|
60
62
|
|