@suss/adapter-ruby 0.34.0 → 0.34.1

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/dist/index.js CHANGED
@@ -19,7 +19,29 @@ function field(node, name) {
19
19
  return node.childForFieldName(name);
20
20
  }
21
21
  function children(node) {
22
- return node.namedChildren.filter((child) => child !== null);
22
+ if (node === null || node === void 0) {
23
+ return [];
24
+ }
25
+ const found = [];
26
+ const count = node.namedChildCount;
27
+ for (let index = 0; index < count; index++) {
28
+ const child = node.namedChild(index);
29
+ if (child !== null) {
30
+ found.push(child);
31
+ }
32
+ }
33
+ return found;
34
+ }
35
+ function allChildren(node) {
36
+ const found = [];
37
+ const count = node.childCount;
38
+ for (let index = 0; index < count; index++) {
39
+ const child = node.child(index);
40
+ if (child !== null) {
41
+ found.push(child);
42
+ }
43
+ }
44
+ return found;
23
45
  }
24
46
  function bodyStatements(body) {
25
47
  return children(body);
@@ -81,6 +103,13 @@ var MODULE_SCOPE_STOPS = /* @__PURE__ */ new Set([
81
103
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
82
104
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
83
105
  var LAMBDA_TYPE = "lambda";
106
+ var RAISING_METHODS = /* @__PURE__ */ new Set(["raise", "fail"]);
107
+ function isRaiseCall(node) {
108
+ if (node.type === "identifier") {
109
+ return RAISING_METHODS.has(node.text);
110
+ }
111
+ return node.type === "call" && field(node, "receiver") === null && RAISING_METHODS.has(field(node, "method")?.text ?? "");
112
+ }
84
113
  function enclosingDefinition(node) {
85
114
  return definitionsAround.of(node);
86
115
  }
@@ -156,7 +185,7 @@ function runStatements(body, blockConfigures = () => false) {
156
185
  }
157
186
  return !(node.type === "call" && blockConfigures(node));
158
187
  };
159
- walkDescendants(body, null, {
188
+ walkDescendants(body, null, children, {
160
189
  at: (node) => {
161
190
  found.push(node);
162
191
  },
@@ -443,6 +472,51 @@ function readCallArgs(argumentList) {
443
472
  }
444
473
  return { positional, keyword };
445
474
  }
475
+ var CONTROL_OPERATORS = /* @__PURE__ */ new Set([
476
+ "and",
477
+ "or",
478
+ "&&",
479
+ "||"
480
+ ]);
481
+ var HANDS_ON = /* @__PURE__ */ new Set(["return", "next", "break"]);
482
+ var isChild = (fieldName) => (parent, child) => field(parent, fieldName)?.id === child.id;
483
+ var always = () => true;
484
+ var asCondition = isChild("condition");
485
+ var VALUE_USED_UNDER = {
486
+ assignment: isChild("right"),
487
+ operator_assignment: isChild("right"),
488
+ call: isChild("receiver"),
489
+ element_reference: always,
490
+ argument_list: (parent) => !HANDS_ON.has(parent.parent?.type ?? ""),
491
+ pair: always,
492
+ array: always,
493
+ hash: always,
494
+ interpolation: always,
495
+ unary: always,
496
+ binary: (parent) => !CONTROL_OPERATORS.has(field(parent, "operator")?.text ?? ""),
497
+ if: asCondition,
498
+ unless: asCondition,
499
+ while: asCondition,
500
+ until: asCondition,
501
+ if_modifier: asCondition,
502
+ unless_modifier: asCondition,
503
+ while_modifier: asCondition,
504
+ until_modifier: asCondition,
505
+ conditional: asCondition,
506
+ case: isChild("value")
507
+ };
508
+ function resultUsedAsValue(node) {
509
+ let child = node;
510
+ let parent = node.parent;
511
+ while (parent !== null && parent.type === "parenthesized_statements" && parent.namedChildCount === 1) {
512
+ child = parent;
513
+ parent = parent.parent;
514
+ }
515
+ if (parent === null) {
516
+ return false;
517
+ }
518
+ return VALUE_USED_UNDER[parent.type]?.(parent, child) ?? false;
519
+ }
446
520
  var NodeSet = class extends IdSet {
447
521
  };
448
522
  var NodeMap = class extends IdMap {
@@ -477,19 +551,23 @@ var PATH_CONVENTIONS = {
477
551
  function createConstantFileCache() {
478
552
  return { autoloadDirectories: /* @__PURE__ */ new Map(), resolved: /* @__PURE__ */ new Map() };
479
553
  }
480
- function resolveConstantFile(root, qualifiedName, convention, acronyms = [], cache = createConstantFileCache()) {
554
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = [], cache = createConstantFileCache(), autoloadRoots = []) {
481
555
  const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
482
- const key = `${root}\0${relative}`;
556
+ const key = [root, ...autoloadRoots, relative].join("\0");
483
557
  const known = cache.resolved.get(key);
484
558
  if (known !== void 0) {
485
559
  return known;
486
560
  }
487
- const found = firstExisting(root, relative, cache);
561
+ const found = firstExisting(root, relative, cache, autoloadRoots);
488
562
  cache.resolved.set(key, found);
489
563
  return found;
490
564
  }
491
- function firstExisting(root, relative, cache) {
492
- for (const candidate of [root, ...autoloadDirectoriesOf(root, cache)]) {
565
+ function firstExisting(root, relative, cache, autoloadRoots) {
566
+ for (const candidate of [
567
+ root,
568
+ ...autoloadDirectoriesOf(root, cache),
569
+ ...autoloadRoots
570
+ ]) {
493
571
  const file = path.join(candidate, relative);
494
572
  if (fs.existsSync(file)) {
495
573
  return file;
@@ -536,10 +614,12 @@ import { nodeOfKey, noteKeyRead, writtenValuesByKey } from "@suss/resolution";
536
614
  import { Evaluator, force, literalOf, text } from "@suss/values";
537
615
 
538
616
  // src/facts/resolve.ts
617
+ import { isTestFile } from "@suss/behavioral-ir";
539
618
  import { constant, lit, rule, variable as v } from "@suss/datalog";
540
619
  import {
541
620
  allocationSitesOf,
542
621
  alsoSteps,
622
+ answersFromTestsLeftOut,
543
623
  askResolution,
544
624
  askResolutionUnder,
545
625
  askSources,
@@ -565,6 +645,71 @@ var CONVERTING_FUNCTIONS = /* @__PURE__ */ new Set([
565
645
  "String",
566
646
  "Float"
567
647
  ]);
648
+ var OBJECT_METHODS = /* @__PURE__ */ new Set([
649
+ "Array",
650
+ "Float",
651
+ "Hash",
652
+ "Integer",
653
+ "String",
654
+ "abort",
655
+ "binding",
656
+ "block_given?",
657
+ "caller",
658
+ "catch",
659
+ "class",
660
+ "clone",
661
+ "define_singleton_method",
662
+ "dup",
663
+ "eql?",
664
+ "equal?",
665
+ "eval",
666
+ "exit",
667
+ "extend",
668
+ "fail",
669
+ "format",
670
+ "freeze",
671
+ "frozen?",
672
+ "hash",
673
+ "inspect",
674
+ "instance_of?",
675
+ "instance_variable_defined?",
676
+ "instance_variable_get",
677
+ "instance_variable_set",
678
+ "instance_variables",
679
+ "is_a?",
680
+ "itself",
681
+ "kind_of?",
682
+ "lambda",
683
+ "loop",
684
+ "method",
685
+ "methods",
686
+ "nil?",
687
+ "object_id",
688
+ "p",
689
+ "pp",
690
+ "print",
691
+ "printf",
692
+ "proc",
693
+ "public_send",
694
+ "puts",
695
+ "raise",
696
+ "rand",
697
+ "require",
698
+ "require_relative",
699
+ "respond_to?",
700
+ "send",
701
+ "singleton_class",
702
+ "sleep",
703
+ "sprintf",
704
+ "srand",
705
+ "system",
706
+ "tap",
707
+ "then",
708
+ "throw",
709
+ "to_s",
710
+ "warn",
711
+ "yield_self"
712
+ ]);
568
713
  var CONSTRUCTOR = "new";
569
714
 
570
715
  // src/facts/resolve.ts
@@ -612,7 +757,13 @@ function resolveEnvObjects(db, objects) {
612
757
  }
613
758
  function writtenValueOf(db, key) {
614
759
  resolveValues(db, [key]);
615
- return sharedWrittenValueOf(db, key, (keys) => resolveValues(db, keys));
760
+ return sharedWrittenValueOf(
761
+ db,
762
+ key,
763
+ (keys) => resolveValues(db, keys),
764
+ void 0,
765
+ answersFromTestsLeftOut(key, isTestFile)
766
+ );
616
767
  }
617
768
  function writtenValueUnder(db, key, site) {
618
769
  const program = resolutionUnderProgram(RUBY_RULES);
@@ -652,7 +803,7 @@ function isFieldOf(parent, node, name) {
652
803
  function namedAt(parent, node) {
653
804
  return isFieldOf(parent, node, "name");
654
805
  }
655
- function always() {
806
+ function always2() {
656
807
  return true;
657
808
  }
658
809
  var SPELLS_A_NAME = {
@@ -667,14 +818,14 @@ var SPELLS_A_NAME = {
667
818
  hash_splat_parameter: namedAt,
668
819
  keyword_parameter: namedAt,
669
820
  block_parameter: namedAt,
670
- destructured_parameter: always,
671
- left_assignment_list: always,
672
- rest_assignment: always,
673
- destructured_left_assignment: always,
674
- exception_variable: always,
675
- undef: always,
676
- alias: always,
677
- ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always]))
821
+ destructured_parameter: always2,
822
+ left_assignment_list: always2,
823
+ rest_assignment: always2,
824
+ destructured_left_assignment: always2,
825
+ exception_variable: always2,
826
+ undef: always2,
827
+ alias: always2,
828
+ ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always2]))
678
829
  };
679
830
  function addNamesUnder(node, names) {
680
831
  if (node === null) {
@@ -1493,9 +1644,7 @@ function emitKeyedElement(emitter, node, parent) {
1493
1644
  if (object === null || isWriteTarget(node, parent)) {
1494
1645
  return;
1495
1646
  }
1496
- const index = node.namedChildren.find(
1497
- (child) => child !== null && child.id !== object.id
1498
- );
1647
+ const index = children(node).find((child) => child.id !== object.id);
1499
1648
  if (index !== void 0) {
1500
1649
  emitKeyedRead(emitter, node, object, index, node);
1501
1650
  emitWrittenKeyRead(emitter, node, object, index);
@@ -1536,17 +1685,17 @@ function fallbackBranchesOf(node, type, enclosing) {
1536
1685
  }
1537
1686
  return [left, right].filter((side) => !raises(side, enclosing));
1538
1687
  }
1539
- var RAISING_METHODS = /* @__PURE__ */ new Set(["raise", "fail"]);
1688
+ var RAISING_METHODS2 = /* @__PURE__ */ new Set(["raise", "fail"]);
1540
1689
  function raises(written, enclosing) {
1541
1690
  const node = readThrough(written);
1542
1691
  if (node.type === "identifier") {
1543
- return RAISING_METHODS.has(node.text) && isBareCall(node, enclosing);
1692
+ return RAISING_METHODS2.has(node.text) && isBareCall(node, enclosing);
1544
1693
  }
1545
1694
  if (node.type !== "call") {
1546
1695
  return false;
1547
1696
  }
1548
1697
  const receiver = field(node, "receiver");
1549
- return RAISING_METHODS.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1698
+ return RAISING_METHODS2.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1550
1699
  }
1551
1700
  function emitExpressionFacts(emitter, node) {
1552
1701
  emitDynamicDefinition(emitter, node, node.type, []);
@@ -2502,9 +2651,7 @@ var EXPRESSION_TYPES = {
2502
2651
  ),
2503
2652
  unary: (node) => {
2504
2653
  const operand2 = field(node, "operand");
2505
- const operator = node.children.find(
2506
- (child) => child !== null && !child.isNamed
2507
- );
2654
+ const operator = allChildren(node).find((child) => !child.isNamed);
2508
2655
  if (operand2 === null || operator === void 0) {
2509
2656
  return OPAQUE;
2510
2657
  }
@@ -2706,8 +2853,25 @@ function statementOf(node) {
2706
2853
  if (appending !== null) {
2707
2854
  return appending;
2708
2855
  }
2856
+ if (isRaiseCall(node) && !rescuedInSameFunction(node)) {
2857
+ return { kind: "throw" };
2858
+ }
2709
2859
  return isTailExpression(node) ? { kind: "return", value: node } : { kind: "expression", value: node };
2710
2860
  }
2861
+ var RESCUING_TYPES = /* @__PURE__ */ new Set(["begin", "body_statement"]);
2862
+ var UNRESCUED_PARTS = /* @__PURE__ */ new Set(["rescue", "else", "ensure"]);
2863
+ function rescuedInSameFunction(node) {
2864
+ let child = node;
2865
+ let parent = node.parent;
2866
+ while (parent !== null && !isRoot(parent)) {
2867
+ if (RESCUING_TYPES.has(parent.type) && !UNRESCUED_PARTS.has(child.type) && children(parent).some((part) => part.type === "rescue")) {
2868
+ return true;
2869
+ }
2870
+ child = parent;
2871
+ parent = parent.parent;
2872
+ }
2873
+ return false;
2874
+ }
2711
2875
  function appendingStatement(node) {
2712
2876
  if (node.type !== "binary" || field(node, "operator")?.text !== "<<") {
2713
2877
  return null;
@@ -3382,6 +3546,10 @@ function writtenNodeOf(node, db, site) {
3382
3546
  const context = db === void 0 ? void 0 : contexts.get(db);
3383
3547
  return context === void 0 ? null : context.writtenTo(node, site);
3384
3548
  }
3549
+ function calledDefinition(call, db) {
3550
+ const context = db === void 0 ? void 0 : contexts.get(db);
3551
+ return context === void 0 ? null : context.callable(call);
3552
+ }
3385
3553
  function askWrittenValues(nodes, db) {
3386
3554
  if (db === void 0 || !contexts.has(db)) {
3387
3555
  return;
@@ -3444,7 +3612,7 @@ function methodDefinitionsIn(file, root) {
3444
3612
  if (METHOD_TYPES.has(node.type)) {
3445
3613
  found.set(nodeId(file, node), node);
3446
3614
  }
3447
- for (const child of node.namedChildren) {
3615
+ for (const child of children(node)) {
3448
3616
  if (child !== null) {
3449
3617
  visit(child);
3450
3618
  }
@@ -3770,12 +3938,27 @@ function graphqlTypeNameFromQualified(qualifiedName, convention) {
3770
3938
 
3771
3939
  // src/ancestry.ts
3772
3940
  async function reachDefinition(qualifiedName, lookup) {
3941
+ const segments = qualifiedName.split("::");
3942
+ for (let length = segments.length; length > 0; length--) {
3943
+ const found = await definedInFileOf(
3944
+ segments.slice(0, length).join("::"),
3945
+ qualifiedName,
3946
+ lookup
3947
+ );
3948
+ if (found !== null) {
3949
+ return found;
3950
+ }
3951
+ }
3952
+ return null;
3953
+ }
3954
+ async function definedInFileOf(fileConstant, qualifiedName, lookup) {
3773
3955
  const filePath = resolveConstantFile(
3774
3956
  lookup.root,
3775
- qualifiedName,
3957
+ fileConstant,
3776
3958
  lookup.pathConvention,
3777
3959
  lookup.acronyms,
3778
- lookup.constantFiles
3960
+ lookup.constantFiles,
3961
+ lookup.autoloadRoots
3779
3962
  );
3780
3963
  if (filePath === null) {
3781
3964
  return null;
@@ -3920,7 +4103,7 @@ function superclassCandidatesOf(blocks) {
3920
4103
  return [];
3921
4104
  }
3922
4105
  function methodInAncestry(ancestry, name, read = {}) {
3923
- for (const entry of ancestry) {
4106
+ for (const [index, entry] of ancestry.entries()) {
3924
4107
  if (entry.type === "root") {
3925
4108
  return { type: "none" };
3926
4109
  }
@@ -3928,7 +4111,8 @@ function methodInAncestry(ancestry, name, read = {}) {
3928
4111
  return {
3929
4112
  type: "unsettled",
3930
4113
  reason: `inherited from ${entry.name}, which this run did not read`,
3931
- cause: "unreadAncestor"
4114
+ cause: "unreadAncestor",
4115
+ definedLater: definedInProjectAfter(ancestry, index, name, read)
3932
4116
  };
3933
4117
  }
3934
4118
  const found = definitionIn(entry.blocks, name, read);
@@ -3945,6 +4129,18 @@ function methodInAncestry(ancestry, name, read = {}) {
3945
4129
  }
3946
4130
  return { type: "none" };
3947
4131
  }
4132
+ function definedInProjectAfter(ancestry, index, name, read) {
4133
+ return ancestry.slice(index + 1).some(
4134
+ (entry) => entry.type !== "root" && entry.type !== "unfollowed" && definitionIn(entry.blocks, name, read).method !== null
4135
+ );
4136
+ }
4137
+ function methodPastUnreadAncestors(ancestry, name, read = {}) {
4138
+ return methodInAncestry(
4139
+ ancestry.filter((entry) => entry.type !== "unfollowed"),
4140
+ name,
4141
+ read
4142
+ );
4143
+ }
3948
4144
  function definitionIn(blocks, name, read) {
3949
4145
  let method = null;
3950
4146
  let block = null;
@@ -3994,11 +4190,17 @@ import {
3994
4190
  graphqlResolverBinding,
3995
4191
  restBinding as restBinding2
3996
4192
  } from "@suss/behavioral-ir";
3997
- import { absentReading as absentReading2, unreadableReading as unreadableReading2 } from "@suss/extractor";
4193
+ import { absentReading as absentReading3, unreadableReading as unreadableReading2 } from "@suss/extractor";
3998
4194
 
3999
4195
  // src/clientCalls.ts
4000
- import { hasNameHole, namesNothing, restBinding } from "@suss/behavioral-ir";
4001
- import { pathOf } from "@suss/values";
4196
+ import {
4197
+ hasNameHole,
4198
+ namesNothing,
4199
+ restBinding,
4200
+ statusMembersOf,
4201
+ testsStatus
4202
+ } from "@suss/behavioral-ir";
4203
+ import { hostOf, pathOf } from "@suss/values";
4002
4204
 
4003
4205
  // src/paths/effects.ts
4004
4206
  import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
@@ -4025,7 +4227,7 @@ function isRaise(node) {
4025
4227
  return false;
4026
4228
  }
4027
4229
  const receiver = field(node, "receiver");
4028
- const method = field(node, "method") ?? node.namedChildren[0] ?? null;
4230
+ const method = field(node, "method") ?? node.namedChild(0) ?? null;
4029
4231
  return receiver === null && method !== null && RAISE_NAMES.has(method.text);
4030
4232
  }
4031
4233
  function exitOf(node) {
@@ -4037,7 +4239,7 @@ function exitOf(node) {
4037
4239
  function exitKindOf(node) {
4038
4240
  let sawReturn = false;
4039
4241
  const raises2 = (current) => {
4040
- for (const child of current.namedChildren) {
4242
+ for (const child of children(current)) {
4041
4243
  if (child === null || CAPTURES_RETURN.has(child.type)) {
4042
4244
  continue;
4043
4245
  }
@@ -4071,13 +4273,52 @@ function callHeaderOf(call, block) {
4071
4273
  function blockStatements(block) {
4072
4274
  return block === null ? [] : children(block);
4073
4275
  }
4276
+ function attachedBlockOf(call) {
4277
+ return children(call).find(
4278
+ (child) => child !== null && (child.type === "do_block" || child.type === "block")
4279
+ ) ?? null;
4280
+ }
4281
+ function soleBlockParameter(block) {
4282
+ const names = children(field(block, "parameters")).filter(
4283
+ (child) => child.type === "identifier"
4284
+ );
4285
+ return names.length === 1 ? names[0].text : null;
4286
+ }
4287
+ function blockBodyStatements(block) {
4288
+ const body = field(block, "body");
4289
+ if (body !== null) {
4290
+ return children(body);
4291
+ }
4292
+ return children(block).filter((child) => child.type !== "block_parameters");
4293
+ }
4294
+ function armBody(block) {
4295
+ return field(block, "body") ?? block;
4296
+ }
4297
+ function formatArmOf(statement, parameter) {
4298
+ if (statement.type !== "call") {
4299
+ return null;
4300
+ }
4301
+ const receiver = field(statement, "receiver");
4302
+ const format = field(statement, "method")?.text;
4303
+ if (receiver?.text !== parameter || format === void 0) {
4304
+ return null;
4305
+ }
4306
+ const block = attachedBlockOf(statement);
4307
+ return {
4308
+ format,
4309
+ header: block === null ? statement.text : callHeaderOf(statement, block).sourceText,
4310
+ block
4311
+ };
4312
+ }
4074
4313
  var Lowerer = class {
4075
- constructor(terminals, responseCalls) {
4314
+ constructor(terminals, responseCalls, formatSwitch) {
4076
4315
  this.terminals = terminals;
4077
4316
  this.responseCalls = responseCalls;
4317
+ this.formatSwitch = formatSwitch;
4078
4318
  }
4079
4319
  terminals;
4080
4320
  responseCalls;
4321
+ formatSwitch;
4081
4322
  terminalsByStmt = /* @__PURE__ */ new Map();
4082
4323
  terminalHome = new NodeMap();
4083
4324
  attachTerminals(statement, found) {
@@ -4108,7 +4349,7 @@ var Lowerer = class {
4108
4349
  found.push(own);
4109
4350
  return found;
4110
4351
  }
4111
- for (const child of node.namedChildren) {
4352
+ for (const child of children(node)) {
4112
4353
  if (child !== null && !OWN_BODY_TYPES.has(child.type)) {
4113
4354
  this.responsesIn(child, found);
4114
4355
  }
@@ -4167,29 +4408,22 @@ var Lowerer = class {
4167
4408
  };
4168
4409
  }
4169
4410
  lowerBegin(node) {
4170
- const rescues = node.namedChildren.filter(
4171
- (child) => child !== null && child.type === "rescue"
4172
- );
4173
- const ensureClause = node.namedChildren.find(
4174
- (child) => child !== null && child.type === "ensure"
4175
- ) ?? null;
4176
- const tryBody = node.namedChildren.filter(
4411
+ const rescues2 = children(node).filter((child) => child.type === "rescue");
4412
+ const ensureClause = children(node).find((child) => child.type === "ensure") ?? null;
4413
+ const tryBody = children(node).filter(
4177
4414
  (child) => child !== null && child.type !== "rescue" && child.type !== "ensure" && child.type !== "else"
4178
4415
  );
4179
4416
  return {
4180
4417
  kind: "try",
4181
4418
  tryBody: tryBody.map((statement) => this.lower(statement)),
4182
- catchBody: rescues.length > 0 ? rescues.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4419
+ catchBody: rescues2.length > 0 ? rescues2.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4183
4420
  finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
4184
4421
  exitKind: exitKindOf(node)
4185
4422
  };
4186
4423
  }
4187
4424
  lowerCase(node) {
4188
4425
  const groups = [];
4189
- for (const child of node.namedChildren) {
4190
- if (child === null) {
4191
- continue;
4192
- }
4426
+ for (const child of children(node)) {
4193
4427
  if (child.type === "when") {
4194
4428
  groups.push({
4195
4429
  condition: handleOf(field(child, "pattern")),
@@ -4207,6 +4441,47 @@ var Lowerer = class {
4207
4441
  }
4208
4442
  return { kind: "switch", groups, exitKind: exitKindOf(node) };
4209
4443
  }
4444
+ /**
4445
+ * `respond_to do |format| ... end` runs one arm per request, picked by
4446
+ * the format the request accepts, so it lowers to a switch with an arm
4447
+ * per `format.x`. The arm that accepts every format is the default.
4448
+ * Without one, a request no arm accepts makes the library raise. Null
4449
+ * when the block contains anything besides those arms.
4450
+ */
4451
+ lowerFormatSwitch(node, block) {
4452
+ const declared = this.formatSwitch;
4453
+ if (declared === void 0 || node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== declared.name) {
4454
+ return null;
4455
+ }
4456
+ const parameter = soleBlockParameter(block);
4457
+ if (parameter === null) {
4458
+ return null;
4459
+ }
4460
+ const arms = blockBodyStatements(block).map(
4461
+ (statement) => formatArmOf(statement, parameter)
4462
+ );
4463
+ if (arms.length === 0 || arms.some((arm) => arm === null)) {
4464
+ return null;
4465
+ }
4466
+ const groups = arms.map(
4467
+ (arm) => ({
4468
+ condition: arm.format === declared.matchesEvery ? null : { sourceText: arm.header, expression: null },
4469
+ hasTrailingBreak: true,
4470
+ body: arm.block === null ? [] : this.lowerBlock(armBody(arm.block))
4471
+ })
4472
+ );
4473
+ const named2 = groups.filter((group) => group.condition !== null);
4474
+ const fallback2 = groups.find((group) => group.condition === null) ?? {
4475
+ condition: null,
4476
+ hasTrailingBreak: true,
4477
+ body: [{ kind: "exit", exit: "throw", exitKind: "throw" }]
4478
+ };
4479
+ return {
4480
+ kind: "switch",
4481
+ groups: [...named2, fallback2],
4482
+ exitKind: exitKindOf(node)
4483
+ };
4484
+ }
4210
4485
  lower(node) {
4211
4486
  const exit = exitOf(node);
4212
4487
  if (exit !== null) {
@@ -4236,10 +4511,12 @@ var Lowerer = class {
4236
4511
  [header]
4237
4512
  );
4238
4513
  }
4239
- const attachedBlock = node.namedChildren.find(
4240
- (child) => child !== null && (child.type === "do_block" || child.type === "block")
4241
- );
4242
- if (node.type === "call" && attachedBlock !== void 0) {
4514
+ const attachedBlock = attachedBlockOf(node);
4515
+ const formatSwitch = attachedBlock === null ? null : this.lowerFormatSwitch(node, attachedBlock);
4516
+ if (formatSwitch !== null) {
4517
+ return this.attach(formatSwitch, [node]);
4518
+ }
4519
+ if (node.type === "call" && attachedBlock !== null) {
4243
4520
  return this.attach(
4244
4521
  {
4245
4522
  kind: "loop",
@@ -4266,10 +4543,11 @@ var Lowerer = class {
4266
4543
  return this.attach({ kind: "opaque", exitKind: exitKindOf(node) }, [node]);
4267
4544
  }
4268
4545
  };
4269
- function lowerRubyBody(body, terminals, responseCalls = []) {
4546
+ function lowerRubyBody(body, terminals, responseCalls = [], formatSwitch) {
4270
4547
  const lowerer = new Lowerer(
4271
4548
  new Map(terminals.map((terminal) => [terminal.id, terminal])),
4272
- new NodeSet(responseCalls)
4549
+ new NodeSet(responseCalls),
4550
+ formatSwitch
4273
4551
  );
4274
4552
  return {
4275
4553
  statements: lowerer.lowerBlock(body),
@@ -4430,6 +4708,14 @@ function enclosingStatement(call, body) {
4430
4708
  return current;
4431
4709
  }
4432
4710
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4711
+ return invocationSites(
4712
+ definitionNode,
4713
+ inherited,
4714
+ keepsArglessCall,
4715
+ facts
4716
+ ).map((site) => site.effect);
4717
+ }
4718
+ function invocationSites(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4433
4719
  const read = methodBody(definitionNode);
4434
4720
  if (read === null) {
4435
4721
  return [];
@@ -4442,7 +4728,7 @@ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, ke
4442
4728
  inherited,
4443
4729
  keepsArglessCall,
4444
4730
  facts
4445
- );
4731
+ ).map((site) => site.effect);
4446
4732
  }
4447
4733
  function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4448
4734
  const body = read.body;
@@ -4476,25 +4762,18 @@ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4476
4762
  statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
4477
4763
  );
4478
4764
  return {
4479
- type: "invocation",
4480
- callee: calleeText(call),
4481
- args: argsOf(call, facts),
4482
- async: false,
4483
- ...conditions.length > 0 ? { preconditions: conditions } : {}
4765
+ call,
4766
+ effect: {
4767
+ type: "invocation",
4768
+ callee: calleeText(call),
4769
+ args: argsOf(call, facts),
4770
+ async: false,
4771
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
4772
+ }
4484
4773
  };
4485
4774
  });
4486
4775
  }
4487
4776
 
4488
- // src/responseStatus.ts
4489
- import {
4490
- absentReading,
4491
- enumerateOrDegrade as enumerateOrDegrade2,
4492
- guardsHoldOn,
4493
- unreadableReading,
4494
- writtenReading
4495
- } from "@suss/extractor";
4496
- import { constantOf as constantOf3, literalOf as literalOf7 } from "@suss/values";
4497
-
4498
4777
  // src/paths/predicates.ts
4499
4778
  var OPERATORS = {
4500
4779
  "==": "eq",
@@ -4513,11 +4792,11 @@ var opaqueOf = (node) => ({
4513
4792
  var JOINING = /* @__PURE__ */ new Set(["&&", "||", "and", "or"]);
4514
4793
  function predicateOf(node, inputs) {
4515
4794
  if (node.type === "parenthesized_statements") {
4516
- const inner = node.namedChildren[0];
4795
+ const inner = node.namedChild(0);
4517
4796
  return inner == null ? opaqueOf(node) : predicateOf(inner, inputs);
4518
4797
  }
4519
4798
  if (node.type === "unary" && field(node, "operator")?.text === "!") {
4520
- const operand2 = field(node, "operand") ?? node.namedChildren[0];
4799
+ const operand2 = field(node, "operand") ?? node.namedChild(0);
4521
4800
  return operand2 == null ? opaqueOf(node) : { type: "negation", operand: predicateOf(operand2, inputs) };
4522
4801
  }
4523
4802
  if (node.type === "binary") {
@@ -4543,11 +4822,11 @@ function predicateOf(node, inputs) {
4543
4822
  }
4544
4823
  function operandsOf(node) {
4545
4824
  if (node.type === "parenthesized_statements") {
4546
- const inner = node.namedChildren[0];
4825
+ const inner = node.namedChild(0);
4547
4826
  return inner == null ? [] : operandsOf(inner);
4548
4827
  }
4549
4828
  if (node.type === "unary" && field(node, "operator")?.text === "!") {
4550
- const operand2 = field(node, "operand") ?? node.namedChildren[0];
4829
+ const operand2 = field(node, "operand") ?? node.namedChild(0);
4551
4830
  return operand2 == null ? [] : operandsOf(operand2);
4552
4831
  }
4553
4832
  if (node.type === "binary") {
@@ -4643,6 +4922,321 @@ function calledMethod(node) {
4643
4922
  return field(node, "method")?.text ?? null;
4644
4923
  }
4645
4924
 
4925
+ // src/responseHandOff.ts
4926
+ import { helperPathsOnVariables, splitAtHandOffs } from "@suss/extractor";
4927
+
4928
+ // src/exceptionClasses.ts
4929
+ var ROOT_EXCEPTION = "Exception";
4930
+ var RUBY_EXCEPTION_PARENTS = {
4931
+ NoMemoryError: ROOT_EXCEPTION,
4932
+ ScriptError: ROOT_EXCEPTION,
4933
+ LoadError: "ScriptError",
4934
+ NotImplementedError: "ScriptError",
4935
+ SyntaxError: "ScriptError",
4936
+ SecurityError: ROOT_EXCEPTION,
4937
+ SignalException: ROOT_EXCEPTION,
4938
+ Interrupt: "SignalException",
4939
+ StandardError: ROOT_EXCEPTION,
4940
+ SystemExit: ROOT_EXCEPTION,
4941
+ SystemStackError: ROOT_EXCEPTION,
4942
+ ArgumentError: "StandardError",
4943
+ UncaughtThrowError: "ArgumentError",
4944
+ EncodingError: "StandardError",
4945
+ FiberError: "StandardError",
4946
+ IOError: "StandardError",
4947
+ EOFError: "IOError",
4948
+ IndexError: "StandardError",
4949
+ KeyError: "IndexError",
4950
+ StopIteration: "IndexError",
4951
+ ClosedQueueError: "StopIteration",
4952
+ LocalJumpError: "StandardError",
4953
+ NameError: "StandardError",
4954
+ NoMethodError: "NameError",
4955
+ RangeError: "StandardError",
4956
+ FloatDomainError: "RangeError",
4957
+ RegexpError: "StandardError",
4958
+ RuntimeError: "StandardError",
4959
+ FrozenError: "RuntimeError",
4960
+ SystemCallError: "StandardError",
4961
+ ThreadError: "StandardError",
4962
+ TypeError: "StandardError",
4963
+ ZeroDivisionError: "StandardError"
4964
+ };
4965
+ var MESSAGE_ONLY_EXCEPTION = "RuntimeError";
4966
+ var ExceptionReader = class {
4967
+ constructor(lookup, library) {
4968
+ this.library = library;
4969
+ this.walking = {
4970
+ ...lookup,
4971
+ ancestryRootClassNames: [
4972
+ ...Object.keys(library),
4973
+ ...Object.keys(RUBY_EXCEPTION_PARENTS),
4974
+ ROOT_EXCEPTION
4975
+ ]
4976
+ };
4977
+ }
4978
+ library;
4979
+ walking;
4980
+ known = /* @__PURE__ */ new Map();
4981
+ read(ref) {
4982
+ const key = [ref.text, ...ref.candidates].join("\0");
4983
+ let found = this.known.get(key);
4984
+ if (found === void 0) {
4985
+ found = readException(ref, this.walking, this.library);
4986
+ this.known.set(key, found);
4987
+ }
4988
+ return found;
4989
+ }
4990
+ };
4991
+ function listedException(name, library) {
4992
+ const fromLibrary = library[name];
4993
+ if (fromLibrary !== void 0) {
4994
+ return {
4995
+ name,
4996
+ ancestors: fromLibrary.ancestors,
4997
+ incomplete: false,
4998
+ inheritableByUnread: false
4999
+ };
5000
+ }
5001
+ const fromRuby = rubyAncestors(name);
5002
+ return fromRuby === null ? null : {
5003
+ name,
5004
+ ancestors: fromRuby,
5005
+ incomplete: false,
5006
+ inheritableByUnread: true
5007
+ };
5008
+ }
5009
+ function unreadException(name) {
5010
+ return {
5011
+ name,
5012
+ ancestors: [ROOT_EXCEPTION],
5013
+ incomplete: true,
5014
+ inheritableByUnread: true
5015
+ };
5016
+ }
5017
+ function classesOf(exception) {
5018
+ return [exception.name, ...exception.ancestors];
5019
+ }
5020
+ async function readException(ref, lookup, library) {
5021
+ for (const candidate of ref.candidates) {
5022
+ const known = listedException(candidate, library);
5023
+ if (known !== null) {
5024
+ return known;
5025
+ }
5026
+ const defined = await reachConstant([candidate], lookup);
5027
+ if (defined !== null) {
5028
+ return projectException(defined, lookup, library);
5029
+ }
5030
+ }
5031
+ return unreadException(ref.candidates.at(-1) ?? ref.text);
5032
+ }
5033
+ async function projectException(defined, lookup, library) {
5034
+ const chain = await ancestryOf(defined.name, defined.blocks, lookup);
5035
+ const ancestors = [];
5036
+ let incomplete = false;
5037
+ for (const entry of chain) {
5038
+ if (entry.type === "bodies" && entry.name === defined.name) {
5039
+ continue;
5040
+ }
5041
+ ancestors.push(entry.name);
5042
+ if (entry.type === "root") {
5043
+ ancestors.push(
5044
+ ...listedException(entry.name, library)?.ancestors ?? []
5045
+ );
5046
+ }
5047
+ if (entry.type === "unfollowed") {
5048
+ incomplete = true;
5049
+ }
5050
+ }
5051
+ if (incomplete && !ancestors.includes(ROOT_EXCEPTION)) {
5052
+ ancestors.push(ROOT_EXCEPTION);
5053
+ }
5054
+ return {
5055
+ name: defined.name,
5056
+ ancestors,
5057
+ incomplete,
5058
+ inheritableByUnread: false
5059
+ };
5060
+ }
5061
+ function rubyAncestors(name) {
5062
+ if (name === ROOT_EXCEPTION) {
5063
+ return [];
5064
+ }
5065
+ if (RUBY_EXCEPTION_PARENTS[name] === void 0) {
5066
+ return null;
5067
+ }
5068
+ const ancestors = [];
5069
+ for (let parent = RUBY_EXCEPTION_PARENTS[name]; parent !== void 0; parent = RUBY_EXCEPTION_PARENTS[parent]) {
5070
+ ancestors.push(parent);
5071
+ }
5072
+ return ancestors;
5073
+ }
5074
+
5075
+ // src/raises.ts
5076
+ var RESCUING_BODIES = /* @__PURE__ */ new Set(["begin", "body_statement"]);
5077
+ var AFTER_CATCH = /* @__PURE__ */ new Set(["rescue", "else", "ensure"]);
5078
+ function escapingRaises(method) {
5079
+ const body = field(method, "body");
5080
+ if (body === null) {
5081
+ return [];
5082
+ }
5083
+ const found = [];
5084
+ const visit = (node) => {
5085
+ for (const child of children(node)) {
5086
+ if (child === null || OWN_BODY_TYPES.has(child.type)) {
5087
+ continue;
5088
+ }
5089
+ if (isRaise(child) && !caughtLocally(child, method)) {
5090
+ found.push(child);
5091
+ continue;
5092
+ }
5093
+ visit(child);
5094
+ }
5095
+ };
5096
+ visit(body);
5097
+ return found;
5098
+ }
5099
+ function caughtLocally(raise, method) {
5100
+ let child = raise;
5101
+ for (let parent = raise.parent; parent !== null && parent.id !== method.id; parent = parent.parent) {
5102
+ if (rescues(parent, child)) {
5103
+ return true;
5104
+ }
5105
+ child = parent;
5106
+ }
5107
+ return false;
5108
+ }
5109
+ function rescues(parent, child) {
5110
+ if (parent.type === "rescue_modifier") {
5111
+ return field(parent, "body")?.id === child.id;
5112
+ }
5113
+ return RESCUING_BODIES.has(parent.type) && !AFTER_CATCH.has(child.type) && children(parent).some((one) => one?.type === "rescue");
5114
+ }
5115
+ function raisedClassRef(call, nesting) {
5116
+ const first = readCallArgs(field(call, "arguments")).positional[0];
5117
+ if (first === void 0) {
5118
+ return null;
5119
+ }
5120
+ if (first.type === "string") {
5121
+ return {
5122
+ text: MESSAGE_ONLY_EXCEPTION,
5123
+ candidates: [MESSAGE_ONLY_EXCEPTION]
5124
+ };
5125
+ }
5126
+ const constant2 = constructedClass(first) ?? first;
5127
+ const candidates = constantRefCandidates(constant2, nesting);
5128
+ return candidates.length === 0 ? null : { text: constant2.text, candidates };
5129
+ }
5130
+ function constructedClass(node) {
5131
+ if (node.type !== "call" || field(node, "method")?.text !== "new") {
5132
+ return null;
5133
+ }
5134
+ return field(node, "receiver");
5135
+ }
5136
+
5137
+ // src/responseHandOff.ts
5138
+ function raiseEndings(method) {
5139
+ return escapingRaises(method).map((call) => ({
5140
+ call,
5141
+ terminal: raiseTerminal(call)
5142
+ }));
5143
+ }
5144
+ function raiseTerminal(call) {
5145
+ const [first, second] = readCallArgs(field(call, "arguments")).positional;
5146
+ const message = first === void 0 ? null : stringLiteralValue(first);
5147
+ return {
5148
+ kind: "throw",
5149
+ statusCode: null,
5150
+ body: null,
5151
+ exceptionType: raisedClassRef(call, [])?.text ?? null,
5152
+ message: message ?? (second === void 0 ? null : stringLiteralValue(second)),
5153
+ component: null,
5154
+ renderTree: null,
5155
+ delegateTarget: null,
5156
+ emitEvent: null,
5157
+ location: rangeOf(call)
5158
+ };
5159
+ }
5160
+ function throughResponseHelpers(branches, sites, options) {
5161
+ const handOffs = [];
5162
+ for (const { call, effect } of sites) {
5163
+ const helper = helperBranchesAt(call, options);
5164
+ if (helper !== null) {
5165
+ handOffs.push({
5166
+ helper,
5167
+ preconditions: effect.preconditions,
5168
+ line: rangeOf(call).start,
5169
+ at: rangeOf(call)
5170
+ });
5171
+ }
5172
+ }
5173
+ return handOffs.length === 0 ? [...branches] : branches.flatMap((branch) => splitAtHandOffs(branch, handOffs));
5174
+ }
5175
+ function helperBranchesAt(call, options) {
5176
+ if (call.type !== "call") {
5177
+ return null;
5178
+ }
5179
+ const args = readCallArgs(field(call, "arguments"));
5180
+ const handedAt = args.positional.flatMap(
5181
+ (argument, position) => argument.type === "identifier" && refersToResponse(argument, options) ? [{ position, variable: argument.text }] : []
5182
+ );
5183
+ const handedAs = Object.entries(args.keyword).flatMap(
5184
+ ([name, argument]) => argument.type === "identifier" && refersToResponse(argument, options) ? [{ name, variable: argument.text }] : []
5185
+ );
5186
+ if (handedAt.length === 0 && handedAs.length === 0) {
5187
+ return null;
5188
+ }
5189
+ const helper = calledDefinition(call, options.facts);
5190
+ if (helper === null) {
5191
+ return null;
5192
+ }
5193
+ const passed = /* @__PURE__ */ new Map();
5194
+ const names = parametersOf(helper).map(
5195
+ (parameter) => paramNameOf(parameter)?.text ?? null
5196
+ );
5197
+ for (const { position, variable } of handedAt) {
5198
+ const name = names[position];
5199
+ if (name !== void 0 && name !== null) {
5200
+ passed.set(name, variable);
5201
+ }
5202
+ }
5203
+ for (const { name, variable } of handedAs) {
5204
+ if (names.includes(name)) {
5205
+ passed.set(name, variable);
5206
+ }
5207
+ }
5208
+ return passed.size === 0 ? null : helperPathsOnVariables(options.readHelper(helper), passed);
5209
+ }
5210
+ function refersToResponse(name, options) {
5211
+ const written = writtenNodeOf(name, options.facts);
5212
+ if (written === null) {
5213
+ return false;
5214
+ }
5215
+ if (sameNode(written, options.request)) {
5216
+ return true;
5217
+ }
5218
+ const member = written.type === "call" ? field(written, "method") : null;
5219
+ const receiver = written.type === "call" ? field(written, "receiver") : null;
5220
+ if (member === null || receiver?.type !== "identifier" || !options.bodyMethods.includes(member.text)) {
5221
+ return false;
5222
+ }
5223
+ const parsedFrom = writtenNodeOf(receiver, options.facts);
5224
+ return parsedFrom !== null && sameNode(parsedFrom, options.request);
5225
+ }
5226
+ function sameNode(a, b) {
5227
+ return a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.tree === b.tree;
5228
+ }
5229
+
5230
+ // src/responseStatus.ts
5231
+ import {
5232
+ absentReading as absentReading2,
5233
+ enumerateOrDegrade as enumerateOrDegrade2,
5234
+ guardsHoldOn,
5235
+ unreadableReading,
5236
+ writtenReading as writtenReading2
5237
+ } from "@suss/extractor";
5238
+ import { constantOf as constantOf3, literalOf as literalOf7 } from "@suss/values";
5239
+
4646
5240
  // src/provenance.ts
4647
5241
  import { sourceRefsOf } from "@suss/extractor";
4648
5242
  import { constantOf as constantOf2, literalOf as literalOf6 } from "@suss/values";
@@ -4710,7 +5304,7 @@ function pickedSource(call, loader) {
4710
5304
  return null;
4711
5305
  }
4712
5306
  const args = field(pick, "arguments");
4713
- const given = args === null ? void 0 : args.namedChildren[source.at];
5307
+ const given = args === null ? void 0 : args.namedChild(source.at);
4714
5308
  return given?.type === "constant" || given?.type === "scope_resolution" ? given : null;
4715
5309
  }
4716
5310
 
@@ -5355,10 +5949,7 @@ function recognizeMethodStorage(method, file, options) {
5355
5949
  return { effects, slots };
5356
5950
  }
5357
5951
  function callsUnder(node, found = []) {
5358
- for (const child of node.namedChildren) {
5359
- if (child === null) {
5360
- continue;
5361
- }
5952
+ for (const child of children(node)) {
5362
5953
  if (child.type === "call") {
5363
5954
  found.push(child);
5364
5955
  }
@@ -5566,6 +6157,111 @@ function literalAt(key, facts) {
5566
6157
  return constant2 !== void 0 ? constant2 : literalOf6(value) ?? void 0;
5567
6158
  }
5568
6159
 
6160
+ // src/raisedStatuses.ts
6161
+ function rescuedBy(handlers, exception) {
6162
+ if (handlers === "eachRoute") {
6163
+ return true;
6164
+ }
6165
+ const classes = new Set(classesOf(exception));
6166
+ return handlers.some(
6167
+ (caught) => caught.someUnread || caught.classes.some(
6168
+ (one) => classes.has(one.name) || exception.incomplete && one.inheritableByUnread
6169
+ )
6170
+ );
6171
+ }
6172
+ function raisedTerminal(pattern, exception, handlers, location) {
6173
+ const libraryStatus = exception === null ? void 0 : pattern.libraryExceptions?.[exception.name]?.status;
6174
+ const status = exception === null || rescuedBy(handlers, exception) ? void 0 : libraryStatus;
6175
+ return {
6176
+ kind: status === void 0 ? "throw" : "response",
6177
+ statusCode: status === void 0 ? null : { type: "literal", value: status },
6178
+ body: null,
6179
+ exceptionType: status === void 0 ? exception?.name ?? null : null,
6180
+ ...status === void 0 && exception !== null ? {
6181
+ exceptionAncestry: {
6182
+ ancestors: exception.ancestors,
6183
+ incomplete: exception.incomplete
6184
+ }
6185
+ } : {},
6186
+ ...status === void 0 && libraryStatus !== void 0 ? { statusWhenUncaught: libraryStatus } : {},
6187
+ message: null,
6188
+ component: null,
6189
+ renderTree: null,
6190
+ delegateTarget: null,
6191
+ emitEvent: null,
6192
+ location
6193
+ };
6194
+ }
6195
+ function raisedStatusBranches(pattern, storage, effects, handlers, location) {
6196
+ const operations = new Set(
6197
+ (effects ?? []).flatMap(
6198
+ (effect) => effect.type === "interaction" && effect.interaction.class === "storage-access" && effect.interaction.operation !== void 0 ? [effect.interaction.operation] : []
6199
+ )
6200
+ );
6201
+ const raised = new Set(
6202
+ storage.flatMap(
6203
+ (one) => (one.raises ?? []).flatMap(
6204
+ (raise) => raise.methods.some((method) => operations.has(method)) ? [raise.exception] : []
6205
+ )
6206
+ )
6207
+ );
6208
+ return [...raised].flatMap((name) => {
6209
+ const exception = listedException(name, pattern.libraryExceptions ?? {}) ?? unreadException(name);
6210
+ const terminal = raisedTerminal(pattern, exception, handlers, location);
6211
+ if (terminal.kind === "throw" && !rescuedBy(handlers, exception)) {
6212
+ return [];
6213
+ }
6214
+ return [
6215
+ {
6216
+ conditions: [
6217
+ {
6218
+ sourceText: `${name} raised`,
6219
+ structured: null,
6220
+ polarity: "positive",
6221
+ source: "earlyThrow"
6222
+ }
6223
+ ],
6224
+ terminal,
6225
+ effects: [],
6226
+ location,
6227
+ isDefault: false
6228
+ }
6229
+ ];
6230
+ });
6231
+ }
6232
+
6233
+ // src/responseBody.ts
6234
+ import { absentReading, writtenReading } from "@suss/extractor";
6235
+ import { shapeOfValue } from "@suss/values";
6236
+ var BODY_KINDS = /* @__PURE__ */ new Set([
6237
+ "record",
6238
+ "sequence",
6239
+ "unbounded"
6240
+ ]);
6241
+ function serializedValue(node, serializers) {
6242
+ const receiver = field(node, "receiver");
6243
+ const method = field(node, "method")?.text;
6244
+ if (node.type !== "call" || receiver === null || method === void 0 || !serializers.has(method) || field(node, "arguments") !== null) {
6245
+ return node;
6246
+ }
6247
+ return serializedValue(receiver, serializers);
6248
+ }
6249
+ function bodyReadingOfCall(call, declaration, facts) {
6250
+ if (declaration.bodyKeyword === void 0 || call.type !== "call") {
6251
+ return absentReading;
6252
+ }
6253
+ const argument = readCallArgs(field(call, "arguments")).keyword[declaration.bodyKeyword];
6254
+ if (argument === void 0) {
6255
+ return absentReading;
6256
+ }
6257
+ const body = serializedValue(
6258
+ argument,
6259
+ new Set(declaration.bodySerializers ?? [])
6260
+ );
6261
+ const value = evaluatedValue(body, facts);
6262
+ return BODY_KINDS.has(value.kind) ? writtenReading(shapeOfValue(value), rangeOf(argument)) : absentReading;
6263
+ }
6264
+
5569
6265
  // src/responseStatus.ts
5570
6266
  function statusArgumentOf(call, declaration) {
5571
6267
  const args = readCallArgs(field(call, "arguments"));
@@ -5578,7 +6274,11 @@ function statusArgumentOf(call, declaration) {
5578
6274
  }
5579
6275
  return args.positional[declaration.statusArgument] ?? null;
5580
6276
  }
5581
- function statusNumberOf(node, names, facts) {
6277
+ function statusNumberOf(node, names, facts, bound = /* @__PURE__ */ new Map()) {
6278
+ const passed = node.type === "identifier" ? bound.get(node.text) : void 0;
6279
+ if (passed !== void 0) {
6280
+ return passed;
6281
+ }
5582
6282
  const value = evaluatedValue(node, facts);
5583
6283
  const constant2 = constantOf3(value);
5584
6284
  if (typeof constant2 === "number") {
@@ -5602,21 +6302,42 @@ function declarationOf(node, byName, locals) {
5602
6302
  const name = field(node, "method")?.text;
5603
6303
  return name === void 0 ? void 0 : byName.get(name);
5604
6304
  }
5605
- function collectResponseCalls(node, byName, locals, found) {
5606
- for (const child of node.namedChildren) {
6305
+ function responderOf(node, responders) {
6306
+ const declaration = declarationOf(node, responders.byName, responders.locals);
6307
+ if (declaration !== void 0) {
6308
+ return { kind: "declared", declaration };
6309
+ }
6310
+ const name = receiverlessName(node, responders.locals);
6311
+ const reading = name === null || responders.helper === void 0 ? null : responders.helper(name, node);
6312
+ return reading === null ? void 0 : { kind: "helper", reading };
6313
+ }
6314
+ function receiverlessName(node, locals) {
6315
+ if (node.type === "identifier") {
6316
+ return isBareMethodCall(node, locals) ? node.text : null;
6317
+ }
6318
+ if (node.type !== "call" || field(node, "receiver") !== null) {
6319
+ return null;
6320
+ }
6321
+ return field(node, "method")?.text ?? null;
6322
+ }
6323
+ function readingOfResponder(call, responder, names, options) {
6324
+ return responder.kind === "helper" ? responder.reading : readingOfCall(call, responder.declaration, names, options);
6325
+ }
6326
+ function collectResponseCalls(node, responders, found) {
6327
+ for (const child of children(node)) {
5607
6328
  if (child === null || OWN_BODY_TYPES.has(child.type)) {
5608
6329
  continue;
5609
6330
  }
5610
- if (declarationOf(child, byName, locals) !== void 0) {
6331
+ if (responderOf(child, responders) !== void 0) {
5611
6332
  found.push(child);
5612
6333
  continue;
5613
6334
  }
5614
- collectResponseCalls(child, byName, locals, found);
6335
+ collectResponseCalls(child, responders, found);
5615
6336
  }
5616
6337
  return found;
5617
6338
  }
5618
6339
  function collectReturns(node, found) {
5619
- for (const child of node.namedChildren) {
6340
+ for (const child of children(node)) {
5620
6341
  if (child === null || OWN_BODY_TYPES.has(child.type)) {
5621
6342
  continue;
5622
6343
  }
@@ -5628,19 +6349,24 @@ function collectReturns(node, found) {
5628
6349
  }
5629
6350
  return found;
5630
6351
  }
5631
- function readingOfCall(call, declaration, names, facts) {
6352
+ function readingOfCall(call, declaration, names, options) {
5632
6353
  const argument = statusArgumentOf(call, declaration);
5633
6354
  if (argument === null) {
5634
- return declaration.defaultStatusCode === void 0 ? absentReading : writtenReading(declaration.defaultStatusCode, rangeOf(call));
6355
+ return declaration.defaultStatusCode === void 0 ? absentReading2 : writtenReading2(declaration.defaultStatusCode, rangeOf(call));
5635
6356
  }
5636
- const status = statusNumberOf(argument, names, facts);
6357
+ const status = statusNumberOf(
6358
+ argument,
6359
+ names,
6360
+ options.facts,
6361
+ options.boundStatuses
6362
+ );
5637
6363
  if (status === null) {
5638
6364
  return unreadableReading(
5639
6365
  "This response writes a status that does not settle on a number here, so this outcome claims none",
5640
6366
  rangeOf(argument)
5641
6367
  );
5642
6368
  }
5643
- return writtenReading(status, rangeOf(argument));
6369
+ return writtenReading2(status, rangeOf(argument));
5644
6370
  }
5645
6371
  function effectsReaching(effects, conditions) {
5646
6372
  return effects.filter(
@@ -5669,6 +6395,45 @@ function conditionInputs(method, paths, facts) {
5669
6395
  );
5670
6396
  return operands.length === 0 ? void 0 : guardInputs(operands, { facts, unit: method });
5671
6397
  }
6398
+ function boundStatusArguments(helper, call, pattern, facts) {
6399
+ const parameters = children(field(helper, "parameters")).filter(
6400
+ (child) => child.type === "identifier"
6401
+ );
6402
+ const args = call.type === "call" ? readCallArgs(field(call, "arguments")).positional : [];
6403
+ const bound = /* @__PURE__ */ new Map();
6404
+ parameters.forEach((parameter, index) => {
6405
+ const arg = args[index];
6406
+ const status = arg === void 0 ? null : statusNumberOf(arg, pattern.statusCodeNames ?? {}, facts);
6407
+ if (status !== null) {
6408
+ bound.set(parameter.text, status);
6409
+ }
6410
+ });
6411
+ return bound;
6412
+ }
6413
+ function helperResponse(helper, pattern, facts, boundStatuses = /* @__PURE__ */ new Map()) {
6414
+ const branches = responseBranches(helper, pattern, [], void 0, {
6415
+ fallthrough: "handOn",
6416
+ facts,
6417
+ boundStatuses
6418
+ });
6419
+ if (branches === null || branches.some((branch) => branch.terminal.kind !== "response")) {
6420
+ return null;
6421
+ }
6422
+ const statuses = new Set(
6423
+ branches.map((branch) => {
6424
+ const reading = branch.statusCodeReading?.reading;
6425
+ if (reading?.kind === "absent") {
6426
+ return branch.statusCodeReading?.libraryDefault ?? null;
6427
+ }
6428
+ return reading?.kind === "written" ? reading.value : null;
6429
+ })
6430
+ );
6431
+ const [only] = [...statuses];
6432
+ return statuses.size === 1 && typeof only === "number" ? writtenReading2(only, rangeOf(helper)) : unreadableReading(
6433
+ "This response is sent by a helper whose status does not settle on one number here, so this outcome claims none",
6434
+ rangeOf(helper)
6435
+ );
6436
+ }
5672
6437
  function branchOf(outcome, pattern, effects, extraEffects, options) {
5673
6438
  if (outcome.fellThrough === true && options.fallthrough === "handOn") {
5674
6439
  return handsOnBranch(outcome, effects, extraEffects);
@@ -5691,6 +6456,7 @@ function branchOf(outcome, pattern, effects, extraEffects, options) {
5691
6456
  reading: outcome.reading,
5692
6457
  libraryDefault: pattern.defaultStatusCode
5693
6458
  },
6459
+ ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
5694
6460
  effects: effectsReaching(effects, outcome.conditions),
5695
6461
  ...extraEffects === void 0 ? {} : { extraEffects },
5696
6462
  location: outcome.location,
@@ -5718,20 +6484,26 @@ function handsOnBranch(outcome, effects, extraEffects) {
5718
6484
  isDefault: outcome.conditions.length === 0
5719
6485
  };
5720
6486
  }
5721
- function returnPathBranches(method, effects, exits = [], facts) {
6487
+ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = []) {
5722
6488
  const body = field(method, "body");
5723
6489
  if (body === null) {
5724
6490
  return null;
5725
6491
  }
5726
6492
  const returns = collectReturns(body, []);
5727
6493
  const exitCalls = exits.map((exit) => exit.call);
5728
- const lowered = lowerRubyBody(body, [...returns, ...exitCalls], exitCalls);
6494
+ const terminals = [
6495
+ ...returns,
6496
+ ...exitCalls,
6497
+ ...raises2.map((raise) => raise.call)
6498
+ ];
6499
+ const lowered = lowerRubyBody(body, terminals, exitCalls);
5729
6500
  const enumerated = enumerateOrDegrade2(
5730
6501
  {
5731
6502
  statements: lowered.statements,
5732
- terminalsByStmt: lowered.terminalsByStmt
6503
+ terminalsByStmt: lowered.terminalsByStmt,
6504
+ ...keepsArms === void 0 ? {} : { keepsArms }
5733
6505
  },
5734
- [...returns, ...exitCalls]
6506
+ terminals
5735
6507
  );
5736
6508
  const inputs = conditionInputs(
5737
6509
  method,
@@ -5759,8 +6531,8 @@ function returnPathBranches(method, effects, exits = [], facts) {
5759
6531
  returnTerminal(rangeOf(statement))
5760
6532
  );
5761
6533
  }
5762
- for (const exit of exits) {
5763
- push(enumerated.byTerminal.get(exit.call) ?? [], exit.terminal);
6534
+ for (const ending of [...exits, ...raises2]) {
6535
+ push(enumerated.byTerminal.get(ending.call) ?? [], ending.terminal);
5764
6536
  }
5765
6537
  push(enumerated.fallthrough, returnTerminal(rangeOf(method)));
5766
6538
  return branches.length === 0 ? null : branches;
@@ -5785,13 +6557,22 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5785
6557
  if (declarations.length === 0 || body === null) {
5786
6558
  return null;
5787
6559
  }
5788
- const byName = declarationsByName(declarations);
5789
- const locals = localNamesIn(method);
5790
- const responses = collectResponseCalls(body, byName, locals, []);
6560
+ const responders = {
6561
+ byName: declarationsByName(declarations),
6562
+ locals: localNamesIn(method),
6563
+ helper: options.respondingHelper
6564
+ };
6565
+ const responses = collectResponseCalls(body, responders, []);
5791
6566
  const returns = collectReturns(body, []);
5792
- const lowered = lowerRubyBody(body, returns, responses);
6567
+ const raises2 = options.raises === void 0 ? [] : escapingRaises(method);
6568
+ const lowered = lowerRubyBody(
6569
+ body,
6570
+ [...returns, ...raises2],
6571
+ responses,
6572
+ pattern.formatSwitch
6573
+ );
5793
6574
  const bareReturns = returns.filter(
5794
- (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
6575
+ (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, responders, []).length === 0
5795
6576
  );
5796
6577
  const terminals = [...responses, ...bareReturns];
5797
6578
  const enumerated = enumerateOrDegrade2(
@@ -5799,7 +6580,7 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5799
6580
  statements: lowered.statements,
5800
6581
  terminalsByStmt: lowered.terminalsByStmt
5801
6582
  },
5802
- terminals
6583
+ [...terminals, ...raises2]
5803
6584
  );
5804
6585
  const statusNames = pattern.statusCodeNames ?? {};
5805
6586
  const inputs = conditionInputs(
@@ -5810,30 +6591,58 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5810
6591
  const conditionsOf = (path7) => path7.map((condition) => conditionOf(condition, inputs));
5811
6592
  const outcomes = [];
5812
6593
  for (const terminal of terminals) {
5813
- const declaration = declarationOf(terminal, byName, locals);
5814
- const reading = declaration === void 0 ? absentReading : readingOfCall(terminal, declaration, statusNames, options.facts);
6594
+ const responder = responderOf(terminal, responders);
6595
+ const reading = responder === void 0 ? absentReading2 : readingOfResponder(terminal, responder, statusNames, options);
6596
+ const body2 = responder?.kind === "declared" ? bodyReadingOfCall(terminal, responder.declaration, options.facts) : void 0;
5815
6597
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
5816
6598
  outcomes.push({
5817
6599
  conditions: conditionsOf(path7),
5818
6600
  reading,
5819
- location: rangeOf(terminal)
6601
+ ...body2 === void 0 ? {} : { body: body2 },
6602
+ location: rangeOf(terminal),
6603
+ // A bare `return` wrote no response, so a filter that takes it
6604
+ // hands the request on the same as one that reaches its end.
6605
+ ...responder === void 0 ? { fellThrough: true } : {}
5820
6606
  });
5821
6607
  }
5822
6608
  }
5823
6609
  for (const path7 of enumerated.fallthrough) {
5824
6610
  outcomes.push({
5825
6611
  conditions: conditionsOf(path7),
5826
- reading: absentReading,
6612
+ reading: absentReading2,
5827
6613
  location: rangeOf(method),
5828
6614
  fellThrough: true
5829
6615
  });
5830
6616
  }
5831
- if (outcomes.length === 0) {
6617
+ const read = options.raises;
6618
+ const raised = raises2.flatMap((call) => {
6619
+ const terminal = raisedTerminal(
6620
+ pattern,
6621
+ read?.classes.get(call) ?? null,
6622
+ read?.handlers ?? [],
6623
+ rangeOf(call)
6624
+ );
6625
+ return (enumerated.byTerminal.get(call) ?? []).map((path7) => {
6626
+ const conditions = conditionsOf(path7);
6627
+ return {
6628
+ conditions,
6629
+ terminal,
6630
+ effects: effectsReaching(effects, conditions),
6631
+ ...extraEffects === void 0 ? {} : { extraEffects },
6632
+ location: terminal.location,
6633
+ isDefault: false
6634
+ };
6635
+ });
6636
+ });
6637
+ if (outcomes.length === 0 && raised.length === 0) {
5832
6638
  return null;
5833
6639
  }
5834
- return outcomes.map(
5835
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
5836
- );
6640
+ return [
6641
+ ...outcomes.map(
6642
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
6643
+ ),
6644
+ ...raised
6645
+ ];
5837
6646
  }
5838
6647
 
5839
6648
  // src/clientCalls.ts
@@ -5846,7 +6655,16 @@ function clientCallUnits(root, pack, pattern, options) {
5846
6655
  }
5847
6656
  for (const call of callsUnder2(method)) {
5848
6657
  for (const request of requestCalls(call, pattern, options)) {
5849
- units.push(clientUnit(method, name, request, pattern, pack, options));
6658
+ units.push(
6659
+ clientUnit(
6660
+ method,
6661
+ name,
6662
+ { ...request, call },
6663
+ pattern,
6664
+ pack,
6665
+ options
6666
+ )
6667
+ );
5850
6668
  }
5851
6669
  }
5852
6670
  }
@@ -5904,10 +6722,7 @@ function requestArgument(call, called, pattern) {
5904
6722
  return pattern.requestObject?.attribute === called ? args.positional[0] : void 0;
5905
6723
  }
5906
6724
  function methodDefinitions(node, found = []) {
5907
- for (const child of node.namedChildren) {
5908
- if (child === null) {
5909
- continue;
5910
- }
6725
+ for (const child of children(node)) {
5911
6726
  if (child.type === "method" || child.type === "singleton_method") {
5912
6727
  found.push(child);
5913
6728
  }
@@ -5916,10 +6731,7 @@ function methodDefinitions(node, found = []) {
5916
6731
  return found;
5917
6732
  }
5918
6733
  function callsUnder2(node, found = []) {
5919
- for (const child of node.namedChildren) {
5920
- if (child === null) {
5921
- continue;
5922
- }
6734
+ for (const child of children(node)) {
5923
6735
  if (child.type === "call") {
5924
6736
  found.push(child);
5925
6737
  }
@@ -5936,7 +6748,7 @@ function requestCalls(call, pattern, options) {
5936
6748
  const read = (site) => requestCall(
5937
6749
  call,
5938
6750
  called,
5939
- receiverPrefix(receiver, pattern, options, site),
6751
+ receiverBase(receiver, pattern, options, site),
5940
6752
  pattern,
5941
6753
  options,
5942
6754
  site
@@ -5949,7 +6761,10 @@ function requestCalls(call, pattern, options) {
5949
6761
  for (const site of constructionSitesOf(call, options.facts)) {
5950
6762
  const stated = read(site);
5951
6763
  if (stated !== null) {
5952
- byBoundary.set(`${stated.method} ${stated.path}`, stated);
6764
+ byBoundary.set(
6765
+ `${stated.method} ${stated.host ?? ""} ${stated.path}`,
6766
+ stated
6767
+ );
5953
6768
  }
5954
6769
  }
5955
6770
  if (byBoundary.size > 0) {
@@ -5957,12 +6772,12 @@ function requestCalls(call, pattern, options) {
5957
6772
  }
5958
6773
  return plain === null ? [] : [plain];
5959
6774
  }
5960
- function requestCall(call, called, prefix, pattern, options, site) {
6775
+ function requestCall(call, called, base, pattern, options, site) {
5961
6776
  const args = readCallArgs(field(call, "arguments"));
5962
6777
  const verb = pattern.verbMethodNames[called];
5963
6778
  if (verb !== void 0) {
5964
- const path8 = urlIn(args, pattern, options, site);
5965
- return path8 === null ? null : { method: verb, path: prefix + path8 };
6779
+ const target2 = targetAt(urlNodeIn(args, pattern), options, site);
6780
+ return target2 === null ? null : { method: verb, ...underBase(base, target2) };
5966
6781
  }
5967
6782
  const sent = pattern.requestObject;
5968
6783
  if (sent === void 0 || sent.attribute !== called) {
@@ -5972,8 +6787,14 @@ function requestCall(call, called, prefix, pattern, options, site) {
5972
6787
  if (built === null) {
5973
6788
  return null;
5974
6789
  }
5975
- const path7 = pathAt(built.url, options, site);
5976
- return path7 === null ? null : { method: built.method, path: prefix + path7 };
6790
+ const target = targetAt(built.url, options, site);
6791
+ return target === null ? null : { method: built.method, ...underBase(base, target) };
6792
+ }
6793
+ function underBase(base, target) {
6794
+ if (base === null || target.host !== void 0) {
6795
+ return target;
6796
+ }
6797
+ return { path: base.path + target.path, host: base.host };
5977
6798
  }
5978
6799
  function requestBuilt(argument, sent, options, site) {
5979
6800
  if (argument === void 0) {
@@ -5994,16 +6815,19 @@ function requestBuilt(argument, sent, options, site) {
5994
6815
  function isLibraryReceiver(receiver, pattern, options) {
5995
6816
  return namesConstant2(receiver, pattern.constantName) || builderCallBehind(receiver, pattern, options.facts) !== null;
5996
6817
  }
5997
- function receiverPrefix(receiver, pattern, options, site) {
6818
+ function receiverBase(receiver, pattern, options, site) {
5998
6819
  const built = builderCallBehind(receiver, pattern, options.facts);
5999
6820
  if (built === null) {
6000
- return "";
6821
+ return null;
6001
6822
  }
6002
6823
  const args = readCallArgs(field(built, "arguments"));
6003
6824
  const keyword = pattern.builderUrlKeyword;
6004
6825
  const base = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[0];
6005
- const path7 = base === void 0 ? null : pathOf(evaluatedValue(base, options.facts, void 0, site));
6006
- return path7 === void 0 || path7 === null ? "" : trimmed(path7);
6826
+ if (base === void 0) {
6827
+ return null;
6828
+ }
6829
+ const value = evaluatedValue(base, options.facts, void 0, site);
6830
+ return { path: trimmed(pathOf(value) ?? ""), host: hostOf(value) };
6007
6831
  }
6008
6832
  function trimmed(path7) {
6009
6833
  return path7 === "/" ? "" : path7.replace(/\/+$/, "");
@@ -6026,22 +6850,32 @@ function builderCallBehind(receiver, pattern, facts) {
6026
6850
  const constant2 = field(written, "receiver");
6027
6851
  return constant2 !== null && namesConstant2(constant2, pattern.constantName) ? written : null;
6028
6852
  }
6029
- function urlIn(args, pattern, options, site) {
6030
- return pathAt(urlNodeIn(args, pattern), options, site);
6031
- }
6032
6853
  function urlNodeIn(args, pattern) {
6033
6854
  const keyword = pattern.url.keyword;
6034
6855
  return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
6035
6856
  }
6036
- function pathAt(written, options, site) {
6857
+ function targetAt(written, options, site) {
6037
6858
  if (written === void 0) {
6038
6859
  return null;
6039
6860
  }
6040
- const path7 = pathOf(evaluatedValue(written, options.facts, void 0, site));
6041
- return path7 === void 0 || namesNothing(path7) ? null : path7;
6861
+ const value = evaluatedValue(written, options.facts, void 0, site);
6862
+ const path7 = pathOf(value);
6863
+ return path7 === void 0 || namesNothing(path7) ? null : { path: path7, host: hostOf(value) };
6042
6864
  }
6043
6865
  function clientUnit(method, name, request, pattern, pack, options) {
6044
6866
  const range = rangeOf(method);
6867
+ const accessors = responseAccessors(pattern);
6868
+ const statusMembers = statusMembersOf(accessors);
6869
+ const reading = {
6870
+ facts: options.facts,
6871
+ statusMembers,
6872
+ handOff: {
6873
+ request: request.call,
6874
+ bodyMethods: accessors.bodyAccessors ?? [],
6875
+ facts: options.facts,
6876
+ readHelper: (helper) => callerBranches(helper, { facts: options.facts, statusMembers })
6877
+ }
6878
+ };
6045
6879
  return {
6046
6880
  identity: {
6047
6881
  name,
@@ -6057,21 +6891,30 @@ function clientUnit(method, name, request, pattern, pack, options) {
6057
6891
  transport: pack.protocol,
6058
6892
  method: request.method,
6059
6893
  path: request.path,
6894
+ host: request.host,
6060
6895
  recognition: pack.name
6061
6896
  }),
6062
6897
  parameters: [],
6063
- branches: callerBranches(method, range, options.facts),
6064
- ...responseAccessors(pattern),
6898
+ branches: callerBranches(method, reading),
6899
+ ...accessors,
6065
6900
  bodyContent: "statements",
6066
6901
  dependencyCalls: [],
6067
6902
  declaredContract: null
6068
6903
  };
6069
6904
  }
6070
- function callerBranches(method, range, facts) {
6071
- const effects = invocationEffects(method, void 0, void 0, facts);
6072
- return returnPathBranches(method, effects, [], facts) ?? [
6073
- returnBranch(range, effects)
6074
- ];
6905
+ function callerBranches(method, reading) {
6906
+ const sites = invocationSites(method, void 0, void 0, reading.facts);
6907
+ const effects = sites.map((site) => site.effect);
6908
+ const keepsArms = (condition) => testsStatus(predicateOf(condition), reading.statusMembers);
6909
+ const found = returnPathBranches(
6910
+ method,
6911
+ effects,
6912
+ [],
6913
+ reading.facts,
6914
+ keepsArms,
6915
+ raiseEndings(method)
6916
+ ) ?? [returnBranch(rangeOf(method), effects)];
6917
+ return reading.handOff === void 0 ? found : throughResponseHelpers(found, sites, reading.handOff);
6075
6918
  }
6076
6919
  function responseAccessors(pattern) {
6077
6920
  const response = pattern.response;
@@ -6082,7 +6925,8 @@ function responseAccessors(pattern) {
6082
6925
  ...response.body === void 0 ? {} : { bodyAccessors: response.body },
6083
6926
  ...response.statusCode === void 0 ? {} : { statusAccessors: response.statusCode },
6084
6927
  ...response.success === void 0 ? {} : { successAccessors: response.success },
6085
- ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery }
6928
+ ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery },
6929
+ ...response.redirectDelivery === void 0 ? {} : { redirectDelivery: response.redirectDelivery }
6086
6930
  };
6087
6931
  }
6088
6932
  function returnBranch(range, effects) {
@@ -6133,9 +6977,7 @@ function elementSite(node) {
6133
6977
  if (object === null || !isEnv(object) || isAssignedTo(node)) {
6134
6978
  return null;
6135
6979
  }
6136
- const index = node.namedChildren.find(
6137
- (child) => child !== null && child.id !== object.id
6138
- );
6980
+ const index = children(node).find((child) => child.id !== object.id);
6139
6981
  if (index === void 0) {
6140
6982
  return null;
6141
6983
  }
@@ -6316,7 +7158,7 @@ function followsExitWhenAbsent(parent, child, subject) {
6316
7158
  if (!STATEMENT_LIST_TYPES.has(parent.type)) {
6317
7159
  return false;
6318
7160
  }
6319
- const statements = parent.namedChildren;
7161
+ const statements = children(parent);
6320
7162
  const at = statements.findIndex((statement) => statement?.id === child.id);
6321
7163
  return statements.slice(0, at).some(
6322
7164
  (statement) => statement !== null && leavesWhenAbsent(statement, subject)
@@ -6368,7 +7210,7 @@ function continuationRaises(from) {
6368
7210
  let parent = current.parent;
6369
7211
  while (parent !== null) {
6370
7212
  if (STATEMENT_LIST_TYPES.has(parent.type)) {
6371
- const statements = parent.namedChildren;
7213
+ const statements = children(parent);
6372
7214
  const at = statements.findIndex((one) => one?.id === current.id);
6373
7215
  const exit = firstExit(statements.slice(at + 1));
6374
7216
  if (exit !== null) {
@@ -6402,20 +7244,13 @@ function firstExit(statements) {
6402
7244
  return null;
6403
7245
  }
6404
7246
  function exitOf2(statement) {
6405
- if (isRaise3(statement)) {
7247
+ if (isRaiseCall(statement)) {
6406
7248
  return "raise";
6407
7249
  }
6408
7250
  if (LEAVING_TYPES.has(statement.type)) {
6409
7251
  return "leave";
6410
7252
  }
6411
- return STATEMENT_LIST_TYPES.has(statement.type) ? firstExit(statement.namedChildren) : null;
6412
- }
6413
- var RAISING_METHODS2 = /* @__PURE__ */ new Set(["raise", "fail"]);
6414
- function isRaise3(node) {
6415
- if (node.type === "identifier") {
6416
- return RAISING_METHODS2.has(node.text);
6417
- }
6418
- return node.type === "call" && field(node, "receiver") === null && RAISING_METHODS2.has(field(node, "method")?.text ?? "");
7253
+ return STATEMENT_LIST_TYPES.has(statement.type) ? firstExit(children(statement)) : null;
6419
7254
  }
6420
7255
  function isPresentWhen(test, outcome, subject) {
6421
7256
  const expression = peelParens(test);
@@ -6483,7 +7318,7 @@ function isNilCheckOn(call, receiver) {
6483
7318
  return call.type === "call" && field(call, "method")?.text === "nil?" && field(call, "receiver")?.id === receiver.id;
6484
7319
  }
6485
7320
  function peelParens(node) {
6486
- const inner = node.type === "parenthesized_statements" && node.namedChildCount === 1 ? node.namedChildren[0] : null;
7321
+ const inner = node.type === "parenthesized_statements" && node.namedChildCount === 1 ? node.namedChild(0) : null;
6487
7322
  return inner === null || inner === void 0 ? node : peelParens(inner);
6488
7323
  }
6489
7324
  function climbParens(node) {
@@ -6528,7 +7363,7 @@ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method",
6528
7363
  function envReadEffects(root, facts) {
6529
7364
  const helpers = helperFactsOf(facts);
6530
7365
  const slots = [];
6531
- walkDescendants2(root, null, {
7366
+ walkDescendants2(root, null, children, {
6532
7367
  at: (node) => {
6533
7368
  const read = envReadAt(node);
6534
7369
  if (read !== null) {
@@ -6608,7 +7443,7 @@ function namedParameters(db) {
6608
7443
  return named2;
6609
7444
  }
6610
7445
  function emitEnvFacts(db, file, root) {
6611
- walkDescendants2(root, null, {
7446
+ walkDescendants2(root, null, children, {
6612
7447
  at: (node) => {
6613
7448
  if (isEnv(node) && handsOnward(node)) {
6614
7449
  db.add("environmentObject", [
@@ -6736,6 +7571,7 @@ function envSiteAt(node) {
6736
7571
  }
6737
7572
 
6738
7573
  // src/filters.ts
7574
+ var RESCUES_NOTHING = { refs: [], someUnread: false };
6739
7575
  function controllerFilters(pattern, ancestry, read = {}) {
6740
7576
  const forms = pattern.filters ?? [];
6741
7577
  if (forms.length === 0) {
@@ -6749,18 +7585,29 @@ function controllerFilters(pattern, ancestry, read = {}) {
6749
7585
  }
6750
7586
  }
6751
7587
  let declared = [];
6752
- for (const { statement } of inheritedStatements(ancestry)) {
7588
+ const inlineSeen = /* @__PURE__ */ new Map();
7589
+ for (const { block, statement } of inheritedStatements(ancestry)) {
6753
7590
  const called = calledName(statement);
6754
7591
  if (called === null) {
6755
7592
  continue;
6756
7593
  }
6757
7594
  const form = byName.get(called);
6758
7595
  if (form !== void 0) {
6759
- for (const declaration of declarationsOf(statement, form, read.facts)) {
6760
- declared = [
6761
- ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
6762
- declaration
6763
- ];
7596
+ const inlineKey = `${block.file}\0${block.info.qualifiedName}\0${form.name}`;
7597
+ const ordinal = inlineSeen.get(inlineKey) ?? 0;
7598
+ const declarations = declarationsOf(
7599
+ statement,
7600
+ form,
7601
+ block,
7602
+ read.facts,
7603
+ ordinal
7604
+ );
7605
+ if (declarations.some((one) => one.inline !== void 0)) {
7606
+ inlineSeen.set(inlineKey, ordinal + 1);
7607
+ }
7608
+ for (const declaration of declarations) {
7609
+ const earlier = declaration.filter.onThrow === true ? void 0 : declared.find((one) => sameFilter(one, declaration));
7610
+ declared = [...declared.filter((one) => one !== earlier), declaration];
6764
7611
  }
6765
7612
  continue;
6766
7613
  }
@@ -6771,62 +7618,105 @@ function controllerFilters(pattern, ancestry, read = {}) {
6771
7618
  }
6772
7619
  const resolved = [];
6773
7620
  for (const declaration of declared) {
6774
- const found = methodInAncestry(ancestry, declaration.methodName, read);
6775
- if (found.type !== "found") {
7621
+ const written = writtenAt(
7622
+ declaration,
7623
+ filterMethod(ancestry, declaration.methodName, read)
7624
+ );
7625
+ if (written === null) {
6776
7626
  continue;
6777
7627
  }
6778
7628
  resolved.push({
6779
7629
  filter: declaration.filter,
6780
- methodName: declaration.methodName,
6781
- method: found.method,
6782
- file: found.block.file,
6783
- enclosingQualifiedName: found.block.info.qualifiedName,
7630
+ ...written,
6784
7631
  only: declaration.only,
6785
- except: declaration.except
7632
+ except: declaration.except,
7633
+ rescues: declaration.rescues
6786
7634
  });
6787
7635
  }
6788
7636
  return [
6789
7637
  ...resolved.filter((one) => one.filter.onThrow !== true),
6790
- ...resolved.filter((one) => one.filter.onThrow === true)
7638
+ ...resolved.filter((one) => one.filter.onThrow === true).reverse()
6791
7639
  ];
6792
7640
  }
7641
+ function writtenAt(declaration, found) {
7642
+ if (found.type === "found") {
7643
+ return {
7644
+ methodName: declaration.methodName,
7645
+ nameKind: "binding",
7646
+ method: found.method,
7647
+ file: found.block.file,
7648
+ enclosingQualifiedName: found.block.info.qualifiedName,
7649
+ nesting: found.block.info.bodyNesting
7650
+ };
7651
+ }
7652
+ const inline = declaration.inline;
7653
+ if (inline === void 0) {
7654
+ return null;
7655
+ }
7656
+ return {
7657
+ methodName: inline.label,
7658
+ nameKind: "label",
7659
+ method: inline.block,
7660
+ file: inline.declaredIn.file,
7661
+ enclosingQualifiedName: inline.declaredIn.info.qualifiedName,
7662
+ nesting: inline.declaredIn.info.bodyNesting
7663
+ };
7664
+ }
7665
+ function filterMethod(ancestry, name, read) {
7666
+ const found = methodInAncestry(ancestry, name, read);
7667
+ if (found.type === "unsettled" && found.cause === "unreadAncestor") {
7668
+ return methodPastUnreadAncestors(ancestry, name, read);
7669
+ }
7670
+ return found;
7671
+ }
6793
7672
  function filterCoversAction(filter, actionName) {
6794
7673
  if (filter.except.has(actionName)) {
6795
7674
  return false;
6796
7675
  }
6797
7676
  return filter.only === null || filter.only.has(actionName);
6798
7677
  }
6799
- function filterReference(filter, displayPath) {
7678
+ function filterReference(filter, displayPath, caught) {
6800
7679
  return {
6801
7680
  file: displayPath,
6802
7681
  name: filter.methodName,
6803
- ...filter.filter.onThrow === true ? { onThrow: true } : {}
7682
+ ...filter.filter.onThrow === true ? { onThrow: true } : {},
7683
+ ...caught === void 0 ? {} : {
7684
+ catches: caught.classes.map((one) => one.name),
7685
+ ...caught.classes.some((one) => one.inheritableByUnread) ? { mayCatchUnreadClasses: true } : {},
7686
+ ...caught.someUnread ? { mayCatchAny: true } : {}
7687
+ }
6804
7688
  };
6805
7689
  }
6806
- function filterUnit(filter, pattern, displayPath, body, facts) {
7690
+ function filterUnit(filter, pattern, displayPath, body, reading = {}) {
6807
7691
  const range = rangeOf(filter.method);
7692
+ const { respondingHelper, raises: raises2 } = reading;
6808
7693
  const branches = responseBranches(
6809
7694
  filter.method,
6810
7695
  pattern,
6811
7696
  body.effects ?? [],
6812
7697
  body.extraEffects,
6813
- { fallthrough: "handOn", facts }
7698
+ {
7699
+ fallthrough: "handOn",
7700
+ facts: reading.facts,
7701
+ ...respondingHelper === void 0 ? {} : { respondingHelper },
7702
+ ...raises2 === void 0 ? {} : { raises: raises2 }
7703
+ }
6814
7704
  );
6815
7705
  return {
6816
7706
  identity: {
6817
7707
  name: filter.methodName,
6818
- nameKind: "binding",
7708
+ nameKind: filter.nameKind,
6819
7709
  kind: "middleware",
6820
7710
  file: displayPath,
6821
7711
  range,
6822
7712
  span: spanOf(filter.method),
6823
- exportName: filter.methodName,
7713
+ exportName: filter.nameKind === "binding" ? filter.methodName : null,
6824
7714
  exportPath: [filter.enclosingQualifiedName, filter.methodName]
6825
7715
  },
6826
7716
  boundaryBinding: null,
6827
7717
  parameters: [],
6828
7718
  branches: withSlotSources(
6829
- branches ?? [handsOn(range, body)],
7719
+ [...branches ?? [handsOn(range, body)], ...reading.raised ?? []],
6830
7720
  body.provenance
6831
7721
  ),
6832
7722
  bodyContent: body.bodyContent ?? "absent",
@@ -6861,25 +7751,92 @@ function calledName(statement) {
6861
7751
  }
6862
7752
  return field(statement, "method")?.text ?? null;
6863
7753
  }
6864
- function declarationsOf(statement, filter, facts) {
7754
+ function declarationsOf(statement, filter, declaredIn, facts, inlineOrdinal) {
6865
7755
  const args = readCallArgs(field(statement, "arguments"));
6866
7756
  const keywords = filter.actionKeywords;
6867
7757
  const only = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.include], facts);
6868
7758
  const except = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.exclude], facts);
6869
- return methodNamesOf(args, filter, facts).map((methodName) => ({
7759
+ const rescues2 = filter.onThrow === true ? rescuedClasses(args, declaredIn.info.bodyNesting, facts) : RESCUES_NOTHING;
7760
+ const inline = inlineHandler(
7761
+ statement,
7762
+ args,
7763
+ filter,
7764
+ declaredIn,
7765
+ inlineOrdinal
7766
+ );
7767
+ return methodNamesOf(args, filter, facts, inline).map((methodName) => ({
6870
7768
  filter,
6871
7769
  methodName,
6872
7770
  only,
6873
- except: except ?? /* @__PURE__ */ new Set()
7771
+ except: except ?? /* @__PURE__ */ new Set(),
7772
+ rescues: rescues2,
7773
+ ...inline === void 0 ? {} : { inline }
6874
7774
  }));
6875
7775
  }
6876
- function methodNamesOf(args, filter, facts) {
7776
+ function inlineHandler(statement, args, filter, declaredIn, ordinal) {
7777
+ const block = blockInPlaceOfMethod(statement, args, filter);
7778
+ if (block === null || blockStatements2(block).length === 0) {
7779
+ return void 0;
7780
+ }
7781
+ return { block, label: `${filter.name}#${ordinal}`, declaredIn };
7782
+ }
7783
+ function rescuedClasses(args, nesting, facts) {
7784
+ const refs = [];
7785
+ let someUnread = args.positional.length === 0;
7786
+ for (const arg of args.positional) {
7787
+ const candidates = constantRefCandidates(arg, nesting);
7788
+ const named2 = candidates.length === 0 ? stringValueOf(arg, facts) : null;
7789
+ if (candidates.length === 0 && named2 === null) {
7790
+ someUnread = true;
7791
+ continue;
7792
+ }
7793
+ refs.push({
7794
+ text: arg.text,
7795
+ candidates: named2 === null ? candidates : [named2]
7796
+ });
7797
+ }
7798
+ return { refs, someUnread };
7799
+ }
7800
+ function methodNamesOf(args, filter, facts, inline) {
7801
+ const named2 = filter.methodFrom === "withKeyword" ? withKeywordName(args, facts) : symbolArgumentNames(args, facts);
7802
+ if (named2.length > 0 || inline === void 0) {
7803
+ return named2;
7804
+ }
7805
+ return [blockHandlerName(inline.block) ?? inline.label];
7806
+ }
7807
+ function withKeywordName(args, facts) {
7808
+ const named2 = args.keyword.with;
7809
+ const value = named2 === void 0 ? null : stringValueOf(named2, facts);
7810
+ return value === null ? [] : [value];
7811
+ }
7812
+ function blockInPlaceOfMethod(statement, args, filter) {
6877
7813
  if (filter.methodFrom === "withKeyword") {
6878
- const named2 = args.keyword.with;
6879
- const value = named2 === void 0 ? null : stringValueOf(named2, facts);
6880
- return value === null ? [] : [value];
7814
+ return args.keyword.with === void 0 ? field(statement, "block") : null;
7815
+ }
7816
+ const lambda = args.positional.find((arg) => arg.type === "lambda");
7817
+ if (lambda !== void 0) {
7818
+ return field(lambda, "body");
6881
7819
  }
6882
- return symbolArgumentNames(args, facts);
7820
+ return args.positional.length === 0 ? field(statement, "block") : null;
7821
+ }
7822
+ function blockStatements2(block) {
7823
+ return children(field(block, "body")).filter(
7824
+ (child) => child.type !== "comment"
7825
+ );
7826
+ }
7827
+ function blockHandlerName(block) {
7828
+ const statements = blockStatements2(block);
7829
+ const only = statements.length === 1 ? statements[0] : void 0;
7830
+ if (only === void 0) {
7831
+ return null;
7832
+ }
7833
+ const parameters = new Set(
7834
+ children(field(block, "parameters")).map((parameter) => parameter.text)
7835
+ );
7836
+ if (only.type === "identifier") {
7837
+ return parameters.has(only.text) ? null : only.text;
7838
+ }
7839
+ return calledName(only);
6883
7840
  }
6884
7841
  function symbolArgumentNames(args, facts) {
6885
7842
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
@@ -6890,13 +7847,16 @@ function skipsOf(statement, filterName, facts) {
6890
7847
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
6891
7848
  }
6892
7849
  function sameFilter(one, other) {
6893
- return one.filter.name === other.filter.name && one.methodName === other.methodName;
7850
+ return namedBySymbol(one) && namedBySymbol(other) && one.filter.name === other.filter.name && one.methodName === other.methodName;
7851
+ }
7852
+ function namedBySymbol(declaration) {
7853
+ return declaration.inline === void 0;
6894
7854
  }
6895
7855
  function applySkips(declared, skips) {
6896
7856
  const left = [];
6897
7857
  for (const declaration of declared) {
6898
7858
  const reaching = skips.filter(
6899
- (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
7859
+ (skip) => namedBySymbol(declaration) && skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
6900
7860
  );
6901
7861
  if (reaching.some((skip) => skip.actions === null)) {
6902
7862
  continue;
@@ -6939,7 +7899,7 @@ var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
6939
7899
  var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
6940
7900
  function streamWriteEffects(root) {
6941
7901
  const found = [];
6942
- walkDescendants3(root, null, {
7902
+ walkDescendants3(root, null, children, {
6943
7903
  at: (node) => {
6944
7904
  const write = node.type === "call" ? streamWriteAt(node) : null;
6945
7905
  if (write !== null) {
@@ -7030,7 +7990,7 @@ function writtenShapeOf(node) {
7030
7990
  return hashShape(node);
7031
7991
  }
7032
7992
  if (node.type === "array") {
7033
- const first = node.namedChildren.find((child) => child !== null);
7993
+ const first = children(node).find((child) => child !== null);
7034
7994
  return {
7035
7995
  type: "array",
7036
7996
  items: first ? writtenShapeOf(first) : { type: "unknown" }
@@ -7040,7 +8000,7 @@ function writtenShapeOf(node) {
7040
8000
  }
7041
8001
  function hashShape(node) {
7042
8002
  const properties = {};
7043
- for (const pair of node.namedChildren) {
8003
+ for (const pair of children(node)) {
7044
8004
  if (pair?.type !== "pair") {
7045
8005
  continue;
7046
8006
  }
@@ -7403,14 +8363,14 @@ var NodesSeen = class {
7403
8363
  };
7404
8364
  function exampleRun(example, pattern, shared) {
7405
8365
  const byName = /* @__PURE__ */ new Map();
7406
- const always2 = [];
8366
+ const always3 = [];
7407
8367
  for (const group of withInlineIncludes(example.groups, pattern, shared)) {
7408
8368
  for (const one of groupBlocks(group, pattern)) {
7409
8369
  for (const name of one.names) {
7410
8370
  byName.set(name, one.block);
7411
8371
  }
7412
8372
  if (one.always) {
7413
- always2.push(one.block);
8373
+ always3.push(one.block);
7414
8374
  }
7415
8375
  }
7416
8376
  }
@@ -7429,7 +8389,7 @@ function exampleRun(example, pattern, shared) {
7429
8389
  }
7430
8390
  };
7431
8391
  include(example.block);
7432
- for (const block of always2) {
8392
+ for (const block of always3) {
7433
8393
  include(block);
7434
8394
  }
7435
8395
  const valueNames = /* @__PURE__ */ new Set([
@@ -8074,6 +9034,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
8074
9034
  },
8075
9035
  lookup: {
8076
9036
  root: pattern.root,
9037
+ autoloadRoots: pattern.autoloadRoots ?? [],
8077
9038
  pathConvention: pattern.pathConvention,
8078
9039
  acronyms: pattern.acronyms ?? [],
8079
9040
  ancestryRootClassNames: pattern.ancestryRootClassNames,
@@ -8229,6 +9190,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8229
9190
  }
8230
9191
  const lookup = {
8231
9192
  root: pattern.root,
9193
+ autoloadRoots: pattern.autoloadRoots ?? [],
8232
9194
  pathConvention: pattern.pathConvention,
8233
9195
  acronyms: pattern.acronyms ?? [],
8234
9196
  ancestryRootClassNames: pattern.ancestryRootClassNames,
@@ -8267,15 +9229,41 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8267
9229
  options.facts,
8268
9230
  options.storage
8269
9231
  );
9232
+ const respondingHelper = respondingHelpersOf(ancestry, pattern, options);
9233
+ const exceptions = new ExceptionReader(
9234
+ lookup,
9235
+ pattern.libraryExceptions ?? {}
9236
+ );
9237
+ const caught = await handlerClasses(filters, exceptions);
9238
+ const handlersAround = (actionName) => filters.filter((filter) => filterCoversAction(filter, actionName)).flatMap((filter) => {
9239
+ const classes = caught.get(filter);
9240
+ return classes === void 0 ? [] : [classes];
9241
+ });
8270
9242
  for (const filter of filters) {
8271
9243
  const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
8272
- const raw = filterUnit(
8273
- filter,
8274
- pattern,
8275
- displayPath,
8276
- bodyOfMethod(filter.method, filter.file, options),
8277
- options.facts
8278
- );
9244
+ const body = bodyOfMethod(filter.method, filter.file, options);
9245
+ const handler = filter.filter.onThrow === true;
9246
+ const raw = filterUnit(filter, pattern, displayPath, body, {
9247
+ facts: options.facts,
9248
+ respondingHelper,
9249
+ ...handler ? {} : {
9250
+ raises: {
9251
+ classes: await raisedClasses(
9252
+ filter.method,
9253
+ filter.nesting,
9254
+ exceptions
9255
+ ),
9256
+ handlers: "eachRoute"
9257
+ },
9258
+ raised: raisedStatusBranches(
9259
+ pattern,
9260
+ options.storage?.patterns ?? [],
9261
+ body.extraEffects,
9262
+ "eachRoute",
9263
+ rangeOf(filter.method)
9264
+ )
9265
+ }
9266
+ });
8279
9267
  units.push(raw);
8280
9268
  options.onReachSeed?.(raw, {
8281
9269
  file: filter.file,
@@ -8283,38 +9271,71 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8283
9271
  enclosingQualifiedName: filter.enclosingQualifiedName
8284
9272
  });
8285
9273
  }
8286
- const emitAction = (actionName, method, block) => {
9274
+ const emitAction = async (actionName, method, block, routes) => {
8287
9275
  const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
8288
9276
  (filter) => filterReference(
8289
9277
  filter,
8290
- options.displayPathOf?.(filter.file) ?? filter.file
9278
+ options.displayPathOf?.(filter.file) ?? filter.file,
9279
+ caught.get(filter)
8291
9280
  )
8292
9281
  );
8293
- const raw = buildControllerActionUnit(
8294
- pack,
8295
- pattern,
8296
- info.qualifiedName,
8297
- actionName,
8298
- method,
8299
- {
8300
- display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
8301
- absolute: block.file
8302
- },
8303
- options,
8304
- around
8305
- );
8306
- units.push(raw);
8307
- options.onReachSeed?.(raw, {
8308
- file: block.file,
8309
- node: method,
8310
- enclosingQualifiedName: block.info.qualifiedName
8311
- });
9282
+ const raises2 = {
9283
+ classes: await raisedClasses(method, block.info.bodyNesting, exceptions),
9284
+ handlers: handlersAround(actionName)
9285
+ };
9286
+ for (const route of routes.length === 0 ? [null] : routes) {
9287
+ const raw = buildControllerActionUnit(
9288
+ pack,
9289
+ pattern,
9290
+ info.qualifiedName,
9291
+ actionName,
9292
+ route,
9293
+ method,
9294
+ {
9295
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
9296
+ absolute: block.file
9297
+ },
9298
+ options,
9299
+ around,
9300
+ respondingHelper,
9301
+ raises2
9302
+ );
9303
+ units.push(raw);
9304
+ options.onReachSeed?.(raw, {
9305
+ file: block.file,
9306
+ node: method,
9307
+ enclosingQualifiedName: block.info.qualifiedName
9308
+ });
9309
+ }
8312
9310
  };
8313
9311
  for (const [actionName, method, block] of actions) {
8314
- emitAction(actionName, method, block);
9312
+ const routes = pattern.routesFor(info.qualifiedName, actionName);
9313
+ await emitAction(actionName, method, block, routes);
8315
9314
  }
8316
9315
  return units;
8317
9316
  }
9317
+ async function handlerClasses(filters, exceptions) {
9318
+ const caught = /* @__PURE__ */ new Map();
9319
+ for (const filter of filters) {
9320
+ if (filter.filter.onThrow !== true) {
9321
+ continue;
9322
+ }
9323
+ const classes = [];
9324
+ for (const ref of filter.rescues.refs) {
9325
+ classes.push(await exceptions.read(ref));
9326
+ }
9327
+ caught.set(filter, { classes, someUnread: filter.rescues.someUnread });
9328
+ }
9329
+ return caught;
9330
+ }
9331
+ async function raisedClasses(method, nesting, exceptions) {
9332
+ const classes = new NodeMap();
9333
+ for (const call of escapingRaises(method)) {
9334
+ const ref = raisedClassRef(call, nesting);
9335
+ classes.set(call, ref === null ? null : await exceptions.read(ref));
9336
+ }
9337
+ return classes;
9338
+ }
8318
9339
  function controllerActions(pattern, info, ownBlocks, ancestry, options) {
8319
9340
  const actions = publicInstanceMethods(ownBlocks, options.bodyBlocks);
8320
9341
  const seen = new Set(actions.map(([actionName]) => actionName));
@@ -8331,7 +9352,7 @@ function controllerActions(pattern, info, ownBlocks, ancestry, options) {
8331
9352
  continue;
8332
9353
  }
8333
9354
  seen.add(actionName);
8334
- if (pattern.routeFor(info.qualifiedName, actionName) !== null) {
9355
+ if (pattern.routesFor(info.qualifiedName, actionName).length > 0) {
8335
9356
  actions.push(action);
8336
9357
  }
8337
9358
  }
@@ -8356,16 +9377,52 @@ function publicInstanceMethods(blocks, bodyBlocks) {
8356
9377
  }
8357
9378
  return found;
8358
9379
  }
8359
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
9380
+ function respondingHelpersOf(ancestry, pattern, options) {
9381
+ const methods = /* @__PURE__ */ new Map();
9382
+ const known = /* @__PURE__ */ new Map();
9383
+ const declared = new Set(
9384
+ (pattern.responseStatusCalls ?? []).map((call) => call.name)
9385
+ );
9386
+ const methodNamed = (name) => {
9387
+ if (!methods.has(name)) {
9388
+ const found = methodInAncestry(ancestry, name, {
9389
+ facts: options.facts,
9390
+ bodyBlocks: options.bodyBlocks,
9391
+ dynamicNames: options.dynamicNames
9392
+ });
9393
+ methods.set(name, found.type === "found" ? found.method : null);
9394
+ }
9395
+ return methods.get(name) ?? null;
9396
+ };
9397
+ return (name, call) => {
9398
+ const method = declared.has(name) ? null : methodNamed(name);
9399
+ if (method === null) {
9400
+ return null;
9401
+ }
9402
+ const bound = boundStatusArguments(method, call, pattern, options.facts);
9403
+ const key = `${name}(${[...bound].join(",")})`;
9404
+ if (!known.has(key)) {
9405
+ known.set(key, helperResponse(method, pattern, options.facts, bound));
9406
+ }
9407
+ return known.get(key) ?? null;
9408
+ };
9409
+ }
9410
+ function servesFormat(pattern, route) {
9411
+ return pattern.optionalFormat === true && route.formatless !== true && !route.path.endsWith("/");
9412
+ }
9413
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, route, method, file, bodyRead, wrappers, respondingHelper, raises2) {
8360
9414
  const range = rangeOf(method);
8361
- const route = pattern.routeFor(controllerQualifiedName, actionName);
8362
9415
  const body = bodyOfMethod(method, file.absolute, bodyRead);
8363
9416
  const perResponse = responseBranches(
8364
9417
  method,
8365
9418
  pattern,
8366
9419
  body.effects ?? [],
8367
9420
  body.extraEffects,
8368
- { facts: bodyRead.facts }
9421
+ {
9422
+ facts: bodyRead.facts,
9423
+ respondingHelper,
9424
+ raises: raises2
9425
+ }
8369
9426
  );
8370
9427
  return {
8371
9428
  identity: {
@@ -8382,7 +9439,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8382
9439
  transport: pack.protocol,
8383
9440
  method: route.method,
8384
9441
  path: route.path,
8385
- recognition: pack.name
9442
+ recognition: pack.name,
9443
+ optionalFormat: servesFormat(pattern, route)
8386
9444
  }),
8387
9445
  parameters: [],
8388
9446
  branches: withSlotSources(
@@ -8402,7 +9460,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8402
9460
  location: range
8403
9461
  },
8404
9462
  statusCodeReading: {
8405
- reading: absentReading2,
9463
+ reading: absentReading3,
8406
9464
  libraryDefault: pattern.defaultStatusCode
8407
9465
  },
8408
9466
  effects: body.effects ?? [],
@@ -8412,6 +9470,14 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8412
9470
  }
8413
9471
  ],
8414
9472
  body.provenance
9473
+ ).concat(
9474
+ raisedStatusBranches(
9475
+ pattern,
9476
+ bodyRead.storage?.patterns ?? [],
9477
+ body.extraEffects,
9478
+ raises2.handlers,
9479
+ range
9480
+ )
8415
9481
  ),
8416
9482
  bodyContent: body.bodyContent ?? "absent",
8417
9483
  dependencyCalls: [],
@@ -9311,7 +10377,7 @@ var EXIT_METHODS = /* @__PURE__ */ new Map([
9311
10377
  var DEFERRED_BODY_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
9312
10378
  function exitSites(root) {
9313
10379
  const found = [];
9314
- walkDescendants4(root, null, {
10380
+ walkDescendants4(root, null, children, {
9315
10381
  at: (node) => {
9316
10382
  const site = node.type === "call" ? exitSiteAt(node) : null;
9317
10383
  if (site !== null) {
@@ -9428,7 +10494,7 @@ function eventsOf(args, called, callbacks, db) {
9428
10494
  import path3 from "path";
9429
10495
  import { SKIP_CHILDREN as SKIP_CHILDREN5, walkDescendants as walkDescendants5 } from "@suss/extractor";
9430
10496
  function emitRequireFacts(db, filePath, root, known) {
9431
- walkDescendants5(root, null, {
10497
+ walkDescendants5(root, null, children, {
9432
10498
  at: (node) => {
9433
10499
  const target = requiredRelativePath(node);
9434
10500
  if (target === null) {
@@ -9469,6 +10535,7 @@ import {
9469
10535
  placeCalls,
9470
10536
  ReceiverClasses,
9471
10537
  recordParameterGaps,
10538
+ StopList,
9472
10539
  TargetPlacements,
9473
10540
  unfollowedCallGap,
9474
10541
  worthRecording
@@ -9510,7 +10577,7 @@ function optionParserFlagReads(method, parameterNames3) {
9510
10577
  return reads;
9511
10578
  }
9512
10579
  function walkBody2(body, at) {
9513
- walkDescendants6(body, null, {
10580
+ walkDescendants6(body, null, children, {
9514
10581
  at,
9515
10582
  into: (node) => DEFERRED_BODY_TYPES4.has(node.type) ? SKIP_CHILDREN6 : null
9516
10583
  });
@@ -9526,7 +10593,7 @@ function flagsDeclaredIn(construction) {
9526
10593
  return [];
9527
10594
  }
9528
10595
  const flags = [];
9529
- walkDescendants6(block, null, {
10596
+ walkDescendants6(block, null, children, {
9530
10597
  at: (node) => {
9531
10598
  if (node.type !== "call" || field(node, "method")?.text !== "on") {
9532
10599
  return;
@@ -9604,6 +10671,11 @@ var UNSETTLED_ON_SELF = {
9604
10671
  reason: "noDeclaration",
9605
10672
  matchByName: true
9606
10673
  };
10674
+ var SELF_CALL_IN_MIXIN = {
10675
+ kind: "stopped",
10676
+ reason: "selfCallInMixin",
10677
+ matchByName: true
10678
+ };
9607
10679
  function followed(target) {
9608
10680
  return { kind: "followed", target };
9609
10681
  }
@@ -9792,22 +10864,27 @@ function leavesRoomForATopLevelMethod(reason) {
9792
10864
  return reason === "noDeclaration" || reason === "outsideRun";
9793
10865
  }
9794
10866
  function resolveImplicitSelf(methodName, site, ctx) {
9795
- if (site.enclosingQualifiedName !== null) {
9796
- const onClass = methodOnSelf(
9797
- site,
9798
- site.enclosingQualifiedName,
9799
- methodName,
9800
- ctx
9801
- );
9802
- if (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason)) {
9803
- return onClass;
9804
- }
10867
+ const onClass = site.enclosingQualifiedName === null ? null : methodOnSelf(site, site.enclosingQualifiedName, methodName, ctx);
10868
+ if (onClass !== null && (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason))) {
10869
+ return onClass;
9805
10870
  }
10871
+ const readWholeAncestry = onClass?.kind === "stopped" && onClass.reason === "noDeclaration";
9806
10872
  const topLevel = resolveTopLevelName(methodName, ctx);
9807
10873
  if (topLevel.kind === "followed" || topLevel.reason !== "noDeclaration") {
9808
10874
  return topLevel;
9809
10875
  }
9810
- return declaredForTheOtherSelf(site, methodName, ctx) ? NO_DECLARATION : UNSETTLED_ON_SELF;
10876
+ if (declaredForTheOtherSelf(site, methodName, ctx)) {
10877
+ return NO_DECLARATION;
10878
+ }
10879
+ return readWholeAncestry && !OBJECT_METHODS.has(methodName) && inModuleInstanceMethod(site, ctx) ? SELF_CALL_IN_MIXIN : UNSETTLED_ON_SELF;
10880
+ }
10881
+ function inModuleInstanceMethod(site, ctx) {
10882
+ const qualifiedName = site.enclosingQualifiedName;
10883
+ if (qualifiedName === null || site.method === null || inClassMethod(site, ctx)) {
10884
+ return false;
10885
+ }
10886
+ const own = ctx.ancestries.get(qualifiedName)?.[0];
10887
+ return own?.type === "bodies" && own.blocks.every((block) => block.info.kind === "module");
9811
10888
  }
9812
10889
  function declaredForTheOtherSelf(site, methodName, ctx) {
9813
10890
  const qualifiedName = site.enclosingQualifiedName;
@@ -9850,12 +10927,16 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
9850
10927
  return followed(reachedMethod(found.method, found.block, methodName));
9851
10928
  }
9852
10929
  if (found.type === "unsettled") {
9853
- return stop(
9854
- found.cause === "dynamicDefine" ? "definedAtLoadTime" : "outsideRun"
9855
- );
10930
+ return stop(unsettledReason(found));
9856
10931
  }
9857
10932
  return NO_DECLARATION;
9858
10933
  }
10934
+ function unsettledReason(found) {
10935
+ if (found.cause === "dynamicDefine") {
10936
+ return "definedAtLoadTime";
10937
+ }
10938
+ return found.definedLater === true ? "behindUnreadAncestor" : "outsideRun";
10939
+ }
9859
10940
  function reachedMethod(node, block, name) {
9860
10941
  return {
9861
10942
  file: block.file,
@@ -10251,7 +11332,7 @@ function scanBody(source, ctx, options, read) {
10251
11332
  );
10252
11333
  const ownParameters = positionalParameters(source.node).map((p) => p.name);
10253
11334
  const followed2 = [];
10254
- const stops = [];
11335
+ const stops = new StopList();
10255
11336
  const placements = new TargetPlacements();
10256
11337
  const receiverClasses = new ReceiverClasses();
10257
11338
  const parameterCalls = [];
@@ -10335,11 +11416,13 @@ function scanBody(source, ctx, options, read) {
10335
11416
  outcome.kind === "followed" ? keyOf(outcome.target) : null
10336
11417
  );
10337
11418
  if (outcome.kind === "stopped") {
10338
- const stopKey = `${outcome.reason}:${callee}`;
10339
11419
  const claimed = options.storage !== void 0 && storageClaims(call, callSite.file, options.storage, callSite.method);
10340
- if (!seen.has(stopKey) && !claimed && worthRecording(outcome.reason)) {
10341
- seen.add(stopKey);
10342
- stops.push({ callee, reason: outcome.reason });
11420
+ if (!claimed && worthRecording(outcome.reason)) {
11421
+ stops.add({
11422
+ callee,
11423
+ reason: outcome.reason,
11424
+ ...resultUsedAsValue(call) ? { usedAsValue: true } : {}
11425
+ });
10343
11426
  }
10344
11427
  if (outcome.reason === "callerSupplied" && !parameterCallsSeen.has(callee)) {
10345
11428
  const receiverName = field(call, "receiver")?.text;
@@ -10362,7 +11445,7 @@ function scanBody(source, ctx, options, read) {
10362
11445
  }
10363
11446
  return {
10364
11447
  followed: followed2,
10365
- stops,
11448
+ stops: stops.stops,
10366
11449
  targets: placements.targets,
10367
11450
  argTargets: placements.argTargets,
10368
11451
  parameterCalls,
@@ -10836,7 +11919,7 @@ import { literalOf as literalOf10 } from "@suss/values";
10836
11919
  function factoriesIn(packs) {
10837
11920
  return packs.flatMap((pack) => pack.factories ?? []);
10838
11921
  }
10839
- function emitFactoryFacts(db, files, isTestFile, patterns, inflections) {
11922
+ function emitFactoryFacts(db, files, isTestFile2, patterns, inflections) {
10840
11923
  for (const pattern of patterns) {
10841
11924
  const definitions = /* @__PURE__ */ new Map();
10842
11925
  for (const { file, root } of files) {
@@ -10844,7 +11927,7 @@ function emitFactoryFacts(db, files, isTestFile, patterns, inflections) {
10844
11927
  }
10845
11928
  const classes = new FactoryClasses(db, definitions, inflections);
10846
11929
  for (const { file, root } of files) {
10847
- if (!isTestFile(file)) {
11930
+ if (!isTestFile2(file)) {
10848
11931
  continue;
10849
11932
  }
10850
11933
  for (const [call, name] of buildsIn(db, root, pattern)) {
@@ -12080,7 +13163,7 @@ function requireTarget(node) {
12080
13163
  }
12081
13164
  function requireSitesIn(root, file) {
12082
13165
  const found = [];
12083
- walkDescendants7(root, null, {
13166
+ walkDescendants7(root, null, children, {
12084
13167
  at: (node) => {
12085
13168
  const target = requireTarget(node);
12086
13169
  if (target !== null) {
@@ -12130,6 +13213,7 @@ export {
12130
13213
  bindEvaluator,
12131
13214
  bodyStatements,
12132
13215
  booleanLiteralValue,
13216
+ children,
12133
13217
  collectFileConstants,
12134
13218
  createConstantFileCache,
12135
13219
  createFileCache,