carrick 0.3.127 → 0.3.128

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.
@@ -130,6 +130,14 @@ const HTTP_V2_RULES = { http: false, message: true, ops: new Set(['request']), m
130
130
  * the method from the member's own name (`get` registers GET) or from a slot.
131
131
  */
132
132
  const SERVER_RULES = { http: false, message: true, ops: new Set(['receive']), methods: ROUTE_METHODS };
133
+ /**
134
+ * A server's rules as `labelledMembers` reads them (carrick#2674): a `send`
135
+ * op is the reply a handler sends back through its reply parameter, on the
136
+ * connection the request came in on, so it names nothing on the wire. Only
137
+ * the stated-parameter reading takes these; `run` keeps `SERVER_RULES`, so
138
+ * no verdict the scanner reads rows from changes.
139
+ */
140
+ const SERVER_REPLY_RULES = { ...SERVER_RULES, ops: new Set(['receive', 'send']), namelessSends: true };
133
141
  /**
134
142
  * The role table: the role alone picks the checks. A role this build does
135
143
  * not read yet (GraphQL executors) answers `role_unsupported`, and `none`
@@ -263,7 +271,8 @@ export class LibraryClaimsVerifier {
263
271
  * A check on an instance or a scope reads the maker or scope claims of the
264
272
  * same request first, whatever their place in it.
265
273
  */
266
- run(fromDir, checks, budgetMs, scanRoot) {
274
+ run(fromDir, checks, budgetMs, scanRoot, options = {}) {
275
+ const { observe, readReplies = false, typePackages = {} } = options;
267
276
  const deadline = performance.now() + budgetMs;
268
277
  const stamp = (check, outcome) => outcome.verdict === 'verified'
269
278
  ? {
@@ -294,7 +303,11 @@ export class LibraryClaimsVerifier {
294
303
  if (check.role === 'http_client' && hasV2Field(check.claim))
295
304
  v2Exports.add(exportKey(check));
296
305
  }
297
- const rulesFor = (check) => check.role === 'http_client' && v2Exports.has(exportKey(check)) ? HTTP_V2_RULES : ROLE_TABLE[check.role];
306
+ const rulesFor = (check) => check.role === 'http_client' && v2Exports.has(exportKey(check))
307
+ ? HTTP_V2_RULES
308
+ : readReplies && check.role === 'server_framework'
309
+ ? SERVER_REPLY_RULES
310
+ : ROLE_TABLE[check.role];
298
311
  for (const check of checks) {
299
312
  const key = exportKey(check);
300
313
  if (!importIndex.has(key)) {
@@ -448,14 +461,19 @@ export class LibraryClaimsVerifier {
448
461
  if (types.some(type => reader.returnSaysNothing(type))) {
449
462
  return { reason: receiverPath.maker ? 'maker_unresolved' : 'export_untyped' };
450
463
  }
464
+ const claim = check.claim;
465
+ const keyBound = claim.kind === 'op' && claim.name !== undefined && 'bound' in claim.name && claim.name.bound === 'key'
466
+ ? verifiedCollects(check.package).length > 0
467
+ : undefined;
451
468
  return {
452
469
  value: types.map(type => ({
453
470
  type,
454
471
  pkg: check.package,
455
- home: reader.homeOf(check.package, [exported.target, ...typeSymbols(type)]),
472
+ home: withTypePackages(reader.homeOf(check.package, [exported.target, ...typeSymbols(type)]), typePackages[check.package]),
456
473
  makerBindsName,
457
474
  scoped: receiverPath.scope !== undefined,
458
475
  makerHolding,
476
+ ...(keyBound === undefined ? {} : { keyBound }),
459
477
  madeBy: receiverPath.maker?.member === null && receiverPath.scope === undefined ? check.export : undefined,
460
478
  })),
461
479
  };
@@ -480,6 +498,32 @@ export class LibraryClaimsVerifier {
480
498
  return { reason: 'unresolved' };
481
499
  return { value: returns };
482
500
  };
501
+ /**
502
+ * The `collects` scope checks of the request (#2474 R6a) that verify,
503
+ * of one package or of any: judged only when a claim reads them, so a
504
+ * request with none judges exactly what it judged before.
505
+ */
506
+ const verifiedCollects = (pkg) => checks
507
+ .map((other, i) => ({ other, i }))
508
+ .filter(({ other }) => other.claim.kind === 'scope' && other.claim.collects !== undefined && (pkg === undefined || other.package === pkg))
509
+ .filter(({ i }) => judge(i).outcome.verdict === 'verified')
510
+ .map(({ i }) => i);
511
+ /**
512
+ * What the request's verified `collects` scopes return, read at their
513
+ * defaults: the collections an adapter's `runs` slot can take, matched
514
+ * across packages (an HTTP framework's adapter serves another
515
+ * package's router). Undefined when there are none.
516
+ */
517
+ const collections = () => {
518
+ const types = new Set();
519
+ for (const i of verifiedCollects()) {
520
+ const built = builtReceiver(checks[i]);
521
+ const read = holdingReturns([judge(i)], built === undefined ? undefined : atDefaults(checks[i], built));
522
+ if ('value' in read)
523
+ read.value.forEach(type => types.add(type));
524
+ }
525
+ return types.size > 0 ? [...types] : undefined;
526
+ };
483
527
  const judge = (index) => {
484
528
  const cached = judged.get(index);
485
529
  if (cached)
@@ -535,16 +579,28 @@ export class LibraryClaimsVerifier {
535
579
  return { outcome: unchecked(receivers.reason) };
536
580
  const built = builtReceiver(check);
537
581
  const builtAtDefaults = built === undefined ? undefined : atDefaults(check, built);
582
+ // An adapter's `runs` is read against the request's collections only
583
+ // when today's reading refuses it (#2474 R6a): `judgeMake` asks.
584
+ const collectionsOf = check.claim.kind === 'make' && check.claim.runs !== undefined ? collections : undefined;
538
585
  const holding = [];
539
586
  const declarations = [];
587
+ const members = [];
588
+ const bodyMembers = [];
540
589
  for (const receiver of receivers.value) {
541
- const result = reader.judgeMessage(receiver, check.claim, rules, builtAtDefaults);
590
+ const result = reader.judgeMessage(receiver, check.claim, rules, builtAtDefaults, collectionsOf);
542
591
  if (result.outcome.verdict !== 'verified')
543
592
  return result;
544
593
  holding.push(...(result.holding ?? []));
545
594
  declarations.push(...(result.outcome.declarations ?? []));
595
+ members.push(...(result.outcome.members ?? []));
596
+ bodyMembers.push(...(result.outcome.bodyMembers ?? []));
546
597
  }
547
- return { outcome: declarations.length > 0 ? { verdict: 'verified', declarations: sortedDeclarations(declarations) } : VERIFIED, holding };
598
+ const verified = declarations.length > 0 ? { verdict: 'verified', declarations: sortedDeclarations(declarations) } : { ...VERIFIED };
599
+ if (verified.verdict === 'verified' && members.length > 0)
600
+ verified.members = members;
601
+ if (verified.verdict === 'verified' && bodyMembers.length > 0)
602
+ verified.bodyMembers = bodyMembers;
603
+ return { outcome: verified, holding };
548
604
  };
549
605
  const semantics = [];
550
606
  for (let index = 0; index < checks.length; index++) {
@@ -553,7 +609,9 @@ export class LibraryClaimsVerifier {
553
609
  semantics.push(stamp(check, unchecked('budget')));
554
610
  continue;
555
611
  }
556
- semantics.push(stamp(check, judge(index).outcome));
612
+ const outcome = judge(index).outcome;
613
+ observe?.(check, outcome);
614
+ semantics.push(stamp(check, outcome));
557
615
  }
558
616
  return {
559
617
  semantics,
@@ -564,6 +622,42 @@ export class LibraryClaimsVerifier {
564
622
  this.project.removeSourceFile(probe);
565
623
  }
566
624
  }
625
+ /**
626
+ * The members a service's server-framework labels name, read with the
627
+ * verifier itself so only a label that verifies in the service's own
628
+ * program counts (carrick#2674): each verified `send` op that carries a
629
+ * `payload` slot, and each verified `receive` op whose `request` names a
630
+ * `body` path. Other checks are not judged.
631
+ */
632
+ labelledMembers(fromDir, checks, budgetMs, scanRoot, typePackages = {}) {
633
+ const sends = new Map();
634
+ const bodies = new Set();
635
+ const wanted = checks.filter(check => check.role === 'server_framework' &&
636
+ check.claim.kind === 'op' &&
637
+ ((check.claim.op === 'send' && check.claim.payload !== undefined) ||
638
+ (check.claim.op === 'receive' && check.claim.request?.body !== undefined)));
639
+ // A receiver an op is read on can need the maker or scope claims beside it.
640
+ const context = checks.filter(check => check.role === 'server_framework' && (check.claim.kind === 'make' || check.claim.kind === 'scope'));
641
+ if (wanted.length === 0)
642
+ return { sends, bodies };
643
+ const outcomes = {};
644
+ const observe = (check, outcome) => {
645
+ if (check.claim.kind !== 'op')
646
+ return;
647
+ const read = outcome.verdict === 'verified' ? 'verified' : `${outcome.verdict} ${outcome.reason}`;
648
+ outcomes[read] = (outcomes[read] ?? 0) + 1;
649
+ if (outcome.verdict !== 'verified')
650
+ return;
651
+ if (check.claim.op === 'send' && check.claim.payload !== undefined) {
652
+ for (const member of outcome.members ?? [])
653
+ sends.set(declarationKey(member), check.claim.payload);
654
+ }
655
+ for (const member of outcome.bodyMembers ?? [])
656
+ bodies.add(declarationKey(member));
657
+ };
658
+ this.run(fromDir, [...context, ...wanted], budgetMs, scanRoot, { observe, readReplies: true, typePackages });
659
+ return { sends, bodies, outcomes };
660
+ }
567
661
  /**
568
662
  * Each specifier's declared surface, read with the verifier's predicates
569
663
  * (carrick#1660): every value export (or only those `only` names for the
@@ -668,7 +762,8 @@ function resolvedBuild(checker, statement) {
668
762
  * array of `[package, exports]` for every surface that listed at least one
669
763
  * export, sorted by `package` (code unit order). A specifier that lists
670
764
  * nothing (unresolved, local, no value exports) is left out, so asking for a
671
- * subpath a version does not have changes nothing. Type text already names
765
+ * subpath a version does not have changes nothing. Handler members are not
766
+ * hashed. Type text already names
672
767
  * the request's directory as `<root>`.
673
768
  */
674
769
  export function fullSurfaceSha256(surfaces) {
@@ -676,7 +771,10 @@ export function fullSurfaceSha256(surfaces) {
676
771
  .filter(surface => surface.reason === undefined && surface.exports.length > 0)
677
772
  .map(surface => [surface.package, surface.exports])
678
773
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
679
- return crypto.createHash('sha256').update(JSON.stringify(listed)).digest('hex');
774
+ // A member's `handlers` (carrick#2674) is out of the hash at every depth, so
775
+ // a package's key does not change when the lister learns to list them.
776
+ const text = JSON.stringify(listed, (key, value) => (key === 'handlers' ? undefined : value));
777
+ return crypto.createHash('sha256').update(text).digest('hex');
680
778
  }
681
779
  function exportKey(check) {
682
780
  return JSON.stringify([check.package, check.export]);
@@ -733,17 +831,31 @@ function placementInvalid(claim) {
733
831
  return of === undefined || (of.maker === undefined && of.scope === undefined);
734
832
  }
735
833
  /**
736
- * Where a handler's request holds its path and method (#2474 R0) is read on
737
- * a receive op with a handler, and names `path` or `url`; every member path
738
- * it gives is non-empty.
834
+ * Where a handler's request holds its path, method and body (#2474 R0,
835
+ * carrick#2674) is read on a receive op with a handler, and names `path`,
836
+ * `url` or `body`; every member path it gives is non-empty.
739
837
  */
740
838
  function requestAtValid(claim) {
741
839
  const request = claim.request;
742
840
  if (claim.op !== 'receive' || claim.handler === undefined)
743
841
  return false;
744
- if (request.path === undefined && request.url === undefined)
842
+ if (request.path === undefined && request.url === undefined && request.body === undefined)
745
843
  return false;
746
- return [request.path, request.url, request.method].every(members => members === undefined || members.length > 0);
844
+ return [request.path, request.url, request.method, request.body].every(members => members === undefined || members.length > 0);
845
+ }
846
+ /** A receiver's home packages, with the type-only packages the store lists for its package. */
847
+ function withTypePackages(home, typePackages) {
848
+ return typePackages && typePackages.length > 0 ? new Set([...home, ...typePackages]) : home;
849
+ }
850
+ /**
851
+ * A declaration's key within one process: its file as the compiler names it
852
+ * and its start. Two programs of one project name a declaration alike, so a
853
+ * label read in the verifier's probe program joins a member read in the
854
+ * inferrer's (carrick#2674).
855
+ */
856
+ export function declarationKey(declaration) {
857
+ const file = declaration.getSourceFile();
858
+ return `${file.fileName}:${declaration.getStart(file)}`;
747
859
  }
748
860
  /**
749
861
  * A message check names a receiver its claim can be read on (contract
@@ -1502,10 +1614,10 @@ class DeclarationReader {
1502
1614
  * overloads it holds on. `built` is the probe's no-argument build of the
1503
1615
  * receiver a maker or scope claim makes (see `returnOf`).
1504
1616
  */
1505
- judgeMessage(receiver, claim, rules, built) {
1617
+ judgeMessage(receiver, claim, rules, built, collections) {
1506
1618
  switch (claim.kind) {
1507
1619
  case 'make':
1508
- return this.judgeMake(receiver, claim, built);
1620
+ return this.judgeMake(receiver, claim, built, collections);
1509
1621
  case 'scope':
1510
1622
  return this.judgeScope(receiver, claim, built);
1511
1623
  case 'op':
@@ -1523,8 +1635,15 @@ class DeclarationReader {
1523
1635
  * defaults (`returnOf`). Those overloads are the ones an instance is read
1524
1636
  * through. A definition (`task({ id, run })`) is a maker with a `name` and a
1525
1637
  * `handler` slot; a queue (`new Queue("emails")`) one with a positional name.
1526
- */
1527
- judgeMake(receiver, claim, built) {
1638
+ *
1639
+ * An adapter (#2474 R6a, contract amendment 3 on carrick#2434) is a maker
1640
+ * whose `runs` slot takes a collection rather than a function. Today's
1641
+ * reading runs first, unchanged; only an overload it refuses on `runs` is
1642
+ * read again, when the request holds verified `collects` scopes
1643
+ * (`collections`), with `runs` taking what one of them returns
1644
+ * (`runsCollectionOutcome`). With no such scope this arm never runs.
1645
+ */
1646
+ judgeMake(receiver, claim, built, collections) {
1528
1647
  const layout = this.layout({
1529
1648
  base: claim.base,
1530
1649
  prefix: claim.prefix,
@@ -1542,6 +1661,12 @@ class DeclarationReader {
1542
1661
  let best;
1543
1662
  for (const signature of callee.signatures) {
1544
1663
  let result = this.messageSignatureOutcome(signature, layout.value, labels);
1664
+ if (result.outcome.verdict !== 'verified' && result.outcome.reason.startsWith('runs_') && collections !== undefined) {
1665
+ const collected = collections();
1666
+ if (collected !== undefined) {
1667
+ result = this.messageSignatureOutcome(signature, layout.value, labels, declared => this.runsCollectionOutcome(signature, declared, collected));
1668
+ }
1669
+ }
1545
1670
  if (result.rank === Infinity && this.returnSaysNothing(this.returnOf(signature, built))) {
1546
1671
  result = { rank: 6, outcome: unchecked('maker_unresolved') };
1547
1672
  }
@@ -1570,12 +1695,23 @@ class DeclarationReader {
1570
1695
  * overloads: a `>scope:` receiver read through it is not resolved.
1571
1696
  * `runs` without `yields` (or the reverse), or `mounts` beside either, is
1572
1697
  * `claim_invalid`.
1698
+ *
1699
+ * Procedure routers (#2474 R6a, contract amendment 3 on carrick#2434): a
1700
+ * scope carries exactly one of `name` and `collects`, and `collects` never
1701
+ * beside `runs`, `yields` or `mounts` (`claim_invalid`). With `collects`
1702
+ * the member takes an object of procedures there (`collectsOutcome`), and
1703
+ * its return, the collection, is read as a plain scope's is.
1573
1704
  */
1574
1705
  judgeScope(receiver, claim, built) {
1575
1706
  if ((claim.runs === undefined) !== (claim.yields === undefined))
1576
1707
  return { outcome: unchecked('claim_invalid') };
1577
1708
  if (claim.mounts !== undefined && claim.runs !== undefined)
1578
1709
  return { outcome: unchecked('claim_invalid') };
1710
+ if ((claim.name === undefined) === (claim.collects === undefined))
1711
+ return { outcome: unchecked('claim_invalid') };
1712
+ if (claim.collects !== undefined && (claim.runs !== undefined || claim.mounts !== undefined)) {
1713
+ return { outcome: unchecked('claim_invalid') };
1714
+ }
1579
1715
  const binds = claim.runs !== undefined || claim.mounts !== undefined;
1580
1716
  const layout = this.layout({ name: claim.name, runs: claim.runs, mounts: claim.mounts });
1581
1717
  if ('failure' in layout)
@@ -1598,6 +1734,11 @@ class DeclarationReader {
1598
1734
  if (mounted)
1599
1735
  result = { rank: 5, outcome: mounted };
1600
1736
  }
1737
+ if (result.rank === Infinity && claim.collects !== undefined) {
1738
+ const collected = this.collectsOutcome(signature, claim.collects);
1739
+ if (collected)
1740
+ result = { rank: 5, outcome: collected };
1741
+ }
1601
1742
  if (result.rank === Infinity && !binds && this.returnSaysNothing(this.returnOf(signature, built))) {
1602
1743
  result = { rank: 6, outcome: unchecked('scope_unresolved') };
1603
1744
  }
@@ -1646,6 +1787,63 @@ class DeclarationReader {
1646
1787
  return erased ? undefined : failed('mounts_not_instance');
1647
1788
  return this.checker.isTypeAssignableTo(receiver.type, slot) ? undefined : failed('mounts_not_instance');
1648
1789
  }
1790
+ /**
1791
+ * `collects: { arg, key?, join }` on a scope (#2474 R6a): the parameter
1792
+ * there is an object type that is not a function, whose keys name the
1793
+ * procedures. A parameter typed `any`, `unknown`, `{}` or by an
1794
+ * unconstrained type parameter says nothing (`collects_untyped`).
1795
+ * Undefined when it holds.
1796
+ */
1797
+ collectsOutcome(signature, collects) {
1798
+ let declared = this.keyParameterAt(signature, collects.arg);
1799
+ if (declared !== undefined && collects.key !== undefined) {
1800
+ const property = this.declaredProperty(declared, collects.key);
1801
+ declared = property ? this.throughConstraint(this.checker.getTypeOfSymbol(property)) : undefined;
1802
+ }
1803
+ if (declared === undefined)
1804
+ return failed('param_missing');
1805
+ if (declared === VARIADIC)
1806
+ return unchecked('collects_untyped');
1807
+ const parts = this.parts(declared);
1808
+ if (parts.length === 0)
1809
+ return failed('collects_not_object');
1810
+ if (parts.some(part => this.isOpenTop(part) || this.isUnconstrained(part) || this.isEmptyObject(part))) {
1811
+ return unchecked('collects_untyped');
1812
+ }
1813
+ if (parts.some(part => !this.isObjectLike(part) || part.getCallSignatures().length > 0)) {
1814
+ return failed('collects_not_object');
1815
+ }
1816
+ return undefined;
1817
+ }
1818
+ /**
1819
+ * An adapter's `runs` slot (#2474 R6a) takes one of `collections`, the
1820
+ * types the request's verified `collects` scopes return, read as a mounted
1821
+ * instance is (`fitsErased`, then assignability). A slot typed `any`,
1822
+ * `unknown`, `{}` or by an unconstrained type parameter says nothing
1823
+ * (`runs_untyped`). Undefined when it holds.
1824
+ */
1825
+ runsCollectionOutcome(signature, declared, collections) {
1826
+ if (declared === undefined)
1827
+ return failed('param_missing');
1828
+ const slot = this.throughConstraint(declared);
1829
+ if (slot === VARIADIC)
1830
+ return unchecked('runs_untyped');
1831
+ const parts = this.parts(slot);
1832
+ if (parts.length === 0)
1833
+ return failed('runs_not_collection');
1834
+ if (parts.some(part => this.isOpenTop(part) || this.isUnconstrained(part) || this.isEmptyObject(part))) {
1835
+ return unchecked('runs_untyped');
1836
+ }
1837
+ const own = signature.typeParameters ?? [];
1838
+ for (const collection of collections) {
1839
+ const erased = this.fitsErased(collection, slot, own);
1840
+ if (erased === true)
1841
+ return undefined;
1842
+ if (erased === undefined && this.checker.isTypeAssignableTo(collection, slot))
1843
+ return undefined;
1844
+ }
1845
+ return failed('runs_not_collection');
1846
+ }
1649
1847
  /**
1650
1848
  * `receiver` fits `slot` with the signature's own type parameters
1651
1849
  * (`own`) erased to their constraints (carrick#2652), or undefined when
@@ -1776,7 +1974,7 @@ class DeclarationReader {
1776
1974
  // what it runs, and one that hands its handler a receiver (a server's
1777
1975
  // connection event) names nothing on the wire itself, so neither needs a
1778
1976
  // name of its own.
1779
- const nameless = claim.runs !== undefined || claim.yields !== undefined;
1977
+ const nameless = claim.runs !== undefined || claim.yields !== undefined || (claim.op === 'send' && rules.namelessSends === true);
1780
1978
  if ((name === undefined && !nameless) || (claim.op === 'send' && claim.payload === undefined)) {
1781
1979
  return unchecked('claim_invalid');
1782
1980
  }
@@ -1785,7 +1983,8 @@ class DeclarationReader {
1785
1983
  nameSlot = undefined;
1786
1984
  }
1787
1985
  else if ('bound' in name) {
1788
- if (name.bound === 'maker' ? !receiver.makerBindsName : !receiver.scoped)
1986
+ const bound = name.bound === 'maker' ? receiver.makerBindsName : name.bound === 'scope' ? receiver.scoped : receiver.keyBound === true;
1987
+ if (!bound)
1789
1988
  return failed('name_unbound');
1790
1989
  }
1791
1990
  else {
@@ -1812,6 +2011,7 @@ class DeclarationReader {
1812
2011
  const labels = claim.key_labels ?? {};
1813
2012
  let best;
1814
2013
  let held = false;
2014
+ const bodyMembers = [];
1815
2015
  for (const signature of callee.signatures) {
1816
2016
  let result = this.messageSignatureOutcome(signature, layout.value, labels);
1817
2017
  if (result.rank === Infinity && claim.yields !== undefined) {
@@ -1820,7 +2020,7 @@ class DeclarationReader {
1820
2020
  result = { rank: 5, outcome: given };
1821
2021
  }
1822
2022
  if (result.rank === Infinity && claim.request !== undefined) {
1823
- const read = this.requestOutcome(signature, claim.handler, claim.request);
2023
+ const read = this.requestOutcome(signature, claim.handler, claim.request, bodyMembers);
1824
2024
  if (read)
1825
2025
  result = { rank: 5, outcome: read };
1826
2026
  }
@@ -1835,7 +2035,15 @@ class DeclarationReader {
1835
2035
  if (boundRuns)
1836
2036
  return this.boundRunsOutcome(receiver.makerHolding, boundRuns);
1837
2037
  const declarations = this.declarationsOf(callee);
1838
- return declarations.length > 0 ? { verdict: 'verified', declarations } : VERIFIED;
2038
+ const members = callee.named
2039
+ ? (callee.named.property.declarations ?? [])
2040
+ : callee.signatures.flatMap(signature => signature.getDeclaration() ?? []);
2041
+ return {
2042
+ verdict: 'verified',
2043
+ ...(declarations.length > 0 ? { declarations } : {}),
2044
+ ...(members.length > 0 ? { members } : {}),
2045
+ ...(bodyMembers.length > 0 ? { bodyMembers } : {}),
2046
+ };
1839
2047
  }
1840
2048
  /**
1841
2049
  * Where a verified op or scope's member is declared, as position rows spell
@@ -1882,6 +2090,18 @@ class DeclarationReader {
1882
2090
  */
1883
2091
  methodOutcome(claim, rules) {
1884
2092
  const words = rules.methods;
2093
+ // A procedure named by its key (#2474 R6a): a receive op with a handler
2094
+ // whose label states its method, a route word. The member names the
2095
+ // procedure kind (`query`), not a verb, so it need not equal the method.
2096
+ if (claim.name !== undefined && 'bound' in claim.name && claim.name.bound === 'key') {
2097
+ if (!words || claim.op !== 'receive' || claim.handler === undefined || claim.method === undefined) {
2098
+ return unchecked('claim_invalid');
2099
+ }
2100
+ if (claim.method_key !== undefined || claim.method_default !== undefined || claim.options !== undefined) {
2101
+ return unchecked('claim_invalid');
2102
+ }
2103
+ return words.has(claim.method) ? undefined : failed('method_not_word');
2104
+ }
1885
2105
  if (!words) {
1886
2106
  const any = claim.method !== undefined ||
1887
2107
  claim.method_key !== undefined ||
@@ -2010,13 +2230,14 @@ class DeclarationReader {
2010
2230
  })));
2011
2231
  }
2012
2232
  /**
2013
- * `request: { param, path?, url?, method? }` on a receive op (#2474 R0):
2014
- * some signature of the handler this overload takes declares a parameter
2015
- * at `param` on which every member path the claim gives resolves, step by
2016
- * step, through properties the library declares (`declaredProperty`).
2017
- * Undefined when it holds.
2233
+ * `request: { param, path?, url?, method?, body? }` on a receive op (#2474
2234
+ * R0, carrick#2674): some signature of the handler this overload takes
2235
+ * declares a parameter at `param` on which every member path the claim
2236
+ * gives resolves, step by step, through properties the library declares
2237
+ * (`declaredProperty`). Undefined when it holds; the declarations of the
2238
+ * member the `body` path ends on are then added to `bodyMembers`.
2018
2239
  */
2019
- requestOutcome(signature, handler, request) {
2240
+ requestOutcome(signature, handler, request, bodyMembers) {
2020
2241
  let declared = this.keyParameterAt(signature, handler.arg);
2021
2242
  if (declared !== undefined && handler.key !== undefined) {
2022
2243
  const property = this.declaredProperty(declared, handler.key);
@@ -2027,25 +2248,33 @@ class DeclarationReader {
2027
2248
  const slot = this.throughConditional(this.throughConstraint(declared));
2028
2249
  if (slot === VARIADIC)
2029
2250
  return unchecked('handler_untyped');
2030
- const paths = [request.path, request.url, request.method].filter((p) => p !== undefined);
2251
+ const paths = [request.path, request.url, request.method, request.body].filter((p) => p !== undefined);
2031
2252
  let best = failed('param_missing');
2032
2253
  for (const part of this.parts(slot)) {
2033
2254
  for (const handed of part.getCallSignatures()) {
2034
2255
  const given = this.parameterAt(handed, request.param);
2035
2256
  if (given === undefined || given === VARIADIC)
2036
2257
  continue;
2258
+ const ends = new Map();
2037
2259
  const resolves = paths.every(members => {
2038
2260
  let at = given;
2261
+ let last;
2039
2262
  for (const member of members) {
2040
2263
  const property = this.declaredProperty(at, member);
2041
2264
  if (!property)
2042
2265
  return false;
2266
+ last = property;
2043
2267
  at = this.checker.getTypeOfSymbol(property);
2044
2268
  }
2269
+ if (last)
2270
+ ends.set(members, last);
2045
2271
  return true;
2046
2272
  });
2047
- if (resolves)
2273
+ if (resolves) {
2274
+ const body = request.body ? ends.get(request.body) : undefined;
2275
+ bodyMembers.push(...(body?.declarations ?? []));
2048
2276
  return undefined;
2277
+ }
2049
2278
  best = failed('request_member_missing');
2050
2279
  }
2051
2280
  }
@@ -2110,8 +2339,12 @@ class DeclarationReader {
2110
2339
  * prefix key accepting a string and a handler key a declared function.
2111
2340
  * 4. Every string slot of the call besides the name is accounted for
2112
2341
  * (design D2, `nameSiblings`).
2342
+ *
2343
+ * `runsAs` replaces the `runs` predicate (an adapter's collection, #2474
2344
+ * R6a); absent, a `runs` slot is a declared function.
2113
2345
  */
2114
- messageSignatureOutcome(signature, layout, labels) {
2346
+ messageSignatureOutcome(signature, layout, labels, runsAs) {
2347
+ const runsFailure = runsAs ?? ((declared) => this.runsFailure(declared));
2115
2348
  for (const [arg, part] of layout.positional) {
2116
2349
  if (part === 'method') {
2117
2350
  const declared = this.keyParameterAt(signature, arg);
@@ -2148,7 +2381,7 @@ class DeclarationReader {
2148
2381
  if (part !== 'handler' && part !== 'ack' && part !== 'runs')
2149
2382
  continue;
2150
2383
  const declared = this.keyParameterAt(signature, arg);
2151
- const failure = part === 'runs' ? this.runsFailure(declared) : this.handlerFailure(declared);
2384
+ const failure = part === 'runs' ? runsFailure(declared) : this.handlerFailure(declared);
2152
2385
  if (failure)
2153
2386
  return { rank: 3, outcome: failure };
2154
2387
  }
@@ -2157,7 +2390,7 @@ class DeclarationReader {
2157
2390
  const slot = this.keyParameterAt(signature, arg);
2158
2391
  if (slot === undefined)
2159
2392
  return { rank: 1, outcome: failed('param_missing') };
2160
- const view = this.viewFor(slot, keys);
2393
+ const view = this.viewFor(slot, keys, runsFailure);
2161
2394
  if ('rank' in view)
2162
2395
  return view;
2163
2396
  views.set(arg, view.view);
@@ -2172,7 +2405,7 @@ class DeclarationReader {
2172
2405
  * union member that is an object type and not a function type, declaring
2173
2406
  * every claimed key with the type its part needs.
2174
2407
  */
2175
- viewFor(slot, keys) {
2408
+ viewFor(slot, keys, runsFailure) {
2176
2409
  if (slot === VARIADIC || !this.isObjectType(slot)) {
2177
2410
  return { rank: 1, outcome: this.slotFailure(slot, 'param_missing') };
2178
2411
  }
@@ -2202,7 +2435,7 @@ class DeclarationReader {
2202
2435
  break;
2203
2436
  }
2204
2437
  if (part === 'runs') {
2205
- const bad = this.runsFailure(type);
2438
+ const bad = runsFailure(type);
2206
2439
  if (bad) {
2207
2440
  failure = { rank: 3, outcome: bad };
2208
2441
  break;
@@ -2769,8 +3002,20 @@ class DeclarationReader {
2769
3002
  dropped += 1;
2770
3003
  return false;
2771
3004
  };
2772
- // Three passes: every export and receiver, then every member name, then signatures.
3005
+ // Handler-parameter members (carrick#2674) have a cap of their own, so they
3006
+ // take nothing from the main one: the listing, `truncated` and the hash are
3007
+ // the ones the lister gave before handlers were listed.
3008
+ let handlerBudget = maxEntries;
3009
+ const takeHandler = () => {
3010
+ if (handlerBudget <= 0)
3011
+ return false;
3012
+ handlerBudget -= 1;
3013
+ return true;
3014
+ };
3015
+ // Four passes: every export and receiver, then every member name, then the
3016
+ // members of each handler's parameters (carrick#2674), then signatures.
2773
3017
  const names = [];
3018
+ const handlers = [];
2774
3019
  const fills = [];
2775
3020
  // What the package's default export names: a maker also exported by name
2776
3021
  // under this declaration is imported the same way, so it is read the same.
@@ -2786,7 +3031,7 @@ class DeclarationReader {
2786
3031
  if (!take())
2787
3032
  return;
2788
3033
  const home = this.homeOf(pkg, [target, ...typeSymbols(receiverType)]);
2789
- entry.receivers.push(this.outlineReceiver(pkg, receiverName, receiverType, home, take, names, fills));
3034
+ entry.receivers.push(this.outlineReceiver(pkg, receiverName, receiverType, home, take, takeHandler, names, handlers, fills));
2790
3035
  };
2791
3036
  receiverOf('export', type);
2792
3037
  for (const maker of this.makers(pkg, target, type)) {
@@ -2803,6 +3048,8 @@ class DeclarationReader {
2803
3048
  }
2804
3049
  for (const run of names)
2805
3050
  run();
3051
+ for (const run of handlers)
3052
+ run();
2806
3053
  for (const fill of fills)
2807
3054
  fill();
2808
3055
  surface.truncated = dropped;
@@ -2947,10 +3194,11 @@ class DeclarationReader {
2947
3194
  }
2948
3195
  /**
2949
3196
  * A receiver, now; its member names later (`names`), once every export and
2950
- * receiver of the package is listed; its call, construct and member
2951
- * signatures last (`fills`), once every name is.
3197
+ * receiver of the package is listed; the members of its handlers'
3198
+ * parameters next (`handlers`), once every name is; its call, construct and
3199
+ * member signatures last (`fills`), once every handler is.
2952
3200
  */
2953
- outlineReceiver(pkg, receiverName, type, home, take, names, fills) {
3201
+ outlineReceiver(pkg, receiverName, type, home, take, takeHandler, names, handlers, fills) {
2954
3202
  const receiver = { receiver: receiverName, members: [] };
2955
3203
  const holder = this.surfaceHolder(pkg, type, home);
2956
3204
  // The receiver's own call and construct signatures, as a claim with a
@@ -2977,11 +3225,102 @@ class DeclarationReader {
2977
3225
  const own = 'signatures' in callee;
2978
3226
  const member = { name: property.getName(), own, signatures: [] };
2979
3227
  receiver.members.push(member);
3228
+ handlers.push(() => {
3229
+ const listed = this.listHandlers(own ? callee.signatures : library, takeHandler);
3230
+ if (listed.length > 0)
3231
+ member.handlers = listed;
3232
+ });
2980
3233
  fills.push(() => (member.signatures = this.listSignatures(own ? callee.signatures : library, holder.type, take)));
2981
3234
  }
2982
3235
  });
2983
3236
  return receiver;
2984
3237
  }
3238
+ /**
3239
+ * The functions a member's signatures take, at a positional argument or at a
3240
+ * key of an options object, with the members their parameters declare. A
3241
+ * handler is listed once per place, from the first signature that states it.
3242
+ * A parameter that declares no member lists nothing, and so does a handler
3243
+ * none of whose parameters do.
3244
+ */
3245
+ listHandlers(signatures, take) {
3246
+ const listed = [];
3247
+ const seen = new Set();
3248
+ const add = (arg, key, slot) => {
3249
+ const place = JSON.stringify([arg, key ?? null]);
3250
+ if (seen.has(place))
3251
+ return;
3252
+ const params = this.listHandlerParams(slot, take);
3253
+ if (params.length === 0)
3254
+ return;
3255
+ seen.add(place);
3256
+ listed.push(key === undefined ? { arg, params } : { arg, key, params });
3257
+ };
3258
+ for (const signature of signatures) {
3259
+ const count = signature.getParameters().length;
3260
+ for (let arg = 0; arg < count; arg++) {
3261
+ const slot = this.keyParameterAt(signature, arg);
3262
+ if (slot === undefined || slot === VARIADIC)
3263
+ continue;
3264
+ if (this.handlerFailure(slot) === undefined) {
3265
+ add(arg, undefined, slot);
3266
+ continue;
3267
+ }
3268
+ if (!this.isObjectType(slot))
3269
+ continue;
3270
+ const keys = new Map();
3271
+ for (const view of this.keyViews(slot)) {
3272
+ for (const property of this.namedProperties(view)) {
3273
+ const type = this.checker.getTypeOfSymbol(property);
3274
+ if (!keys.has(property.getName()) && this.handlerFailure(type) === undefined)
3275
+ keys.set(property.getName(), type);
3276
+ }
3277
+ }
3278
+ for (const key of [...keys.keys()].sort(byName))
3279
+ add(arg, key, keys.get(key));
3280
+ }
3281
+ }
3282
+ return listed;
3283
+ }
3284
+ /** The parameters of a function type's first signature that declare members, each member counting against the cap. */
3285
+ listHandlerParams(handler, take) {
3286
+ const slot = this.throughConditional(this.throughConstraint(handler));
3287
+ if (slot === VARIADIC)
3288
+ return [];
3289
+ const signature = this.parts(slot)
3290
+ .flatMap(part => part.getCallSignatures())
3291
+ .find(each => each.getParameters().length > 0);
3292
+ if (!signature)
3293
+ return [];
3294
+ const listed = [];
3295
+ signature.getParameters().forEach((parameter, index) => {
3296
+ const type = this.checker.getTypeOfSymbol(parameter);
3297
+ if (this.namedProperties(type).length === 0 || !take())
3298
+ return;
3299
+ const members = [];
3300
+ this.listMembers(type, [], members, take);
3301
+ listed.push({ param: index, name: parameter.getName(), type: this.printed(type), members });
3302
+ });
3303
+ return listed;
3304
+ }
3305
+ /** A type's declared members by path, parents before children, each level sorted by name, down to `HANDLER_MEMBER_DEPTH`. */
3306
+ listMembers(type, prefix, into, take) {
3307
+ for (const property of sortedByName(this.namedProperties(type))) {
3308
+ if (!take())
3309
+ continue;
3310
+ const memberType = this.checker.getTypeOfSymbol(property);
3311
+ const path = [...prefix, property.getName()];
3312
+ const callable = this.librarySignatures(memberType, 'call').length > 0;
3313
+ into.push({
3314
+ path,
3315
+ type: this.printed(memberType),
3316
+ optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
3317
+ function: callable,
3318
+ });
3319
+ if (!callable && path.length < HANDLER_MEMBER_DEPTH && this.isObjectType(memberType)) {
3320
+ this.listMembers(memberType, path, into, take);
3321
+ }
3322
+ }
3323
+ }
2985
3324
  listSignatures(signatures, holder, take) {
2986
3325
  const listed = [];
2987
3326
  for (const signature of signatures) {
@@ -3532,6 +3871,8 @@ class DeclarationReader {
3532
3871
  * union the builder cuts short keeps the members it made first, so the
3533
3872
  * listing orders every union before its own cut (`truncate`).
3534
3873
  */
3874
+ /** The longest member path a handler parameter's members are listed to (`['raw', 'url']`). */
3875
+ const HANDLER_MEMBER_DEPTH = 2;
3535
3876
  const PRINT_FLAGS = ts.NodeBuilderFlags.AllowUniqueESSymbolType |
3536
3877
  ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope |
3537
3878
  ts.NodeBuilderFlags.IgnoreErrors |