@suss/adapter-ruby 0.34.0 → 0.35.0

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.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  bindEvaluator: () => bindEvaluator,
37
37
  bodyStatements: () => bodyStatements,
38
38
  booleanLiteralValue: () => booleanLiteralValue,
39
+ children: () => children,
39
40
  collectFileConstants: () => collectFileConstants,
40
41
  createConstantFileCache: () => createConstantFileCache,
41
42
  createFileCache: () => createFileCache,
@@ -97,7 +98,29 @@ function field(node, name) {
97
98
  return node.childForFieldName(name);
98
99
  }
99
100
  function children(node) {
100
- return node.namedChildren.filter((child) => child !== null);
101
+ if (node === null || node === void 0) {
102
+ return [];
103
+ }
104
+ const found = [];
105
+ const count = node.namedChildCount;
106
+ for (let index = 0; index < count; index++) {
107
+ const child = node.namedChild(index);
108
+ if (child !== null) {
109
+ found.push(child);
110
+ }
111
+ }
112
+ return found;
113
+ }
114
+ function allChildren(node) {
115
+ const found = [];
116
+ const count = node.childCount;
117
+ for (let index = 0; index < count; index++) {
118
+ const child = node.child(index);
119
+ if (child !== null) {
120
+ found.push(child);
121
+ }
122
+ }
123
+ return found;
101
124
  }
102
125
  function bodyStatements(body) {
103
126
  return children(body);
@@ -159,6 +182,13 @@ var MODULE_SCOPE_STOPS = /* @__PURE__ */ new Set([
159
182
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
160
183
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
161
184
  var LAMBDA_TYPE = "lambda";
185
+ var RAISING_METHODS = /* @__PURE__ */ new Set(["raise", "fail"]);
186
+ function isRaiseCall(node) {
187
+ if (node.type === "identifier") {
188
+ return RAISING_METHODS.has(node.text);
189
+ }
190
+ return node.type === "call" && field(node, "receiver") === null && RAISING_METHODS.has(field(node, "method")?.text ?? "");
191
+ }
162
192
  function enclosingDefinition(node) {
163
193
  return definitionsAround.of(node);
164
194
  }
@@ -234,7 +264,7 @@ function runStatements(body, blockConfigures = () => false) {
234
264
  }
235
265
  return !(node.type === "call" && blockConfigures(node));
236
266
  };
237
- (0, import_extractor.walkDescendants)(body, null, {
267
+ (0, import_extractor.walkDescendants)(body, null, children, {
238
268
  at: (node) => {
239
269
  found.push(node);
240
270
  },
@@ -320,8 +350,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
320
350
  "protected",
321
351
  "public"
322
352
  ]);
323
- function visibilityKeyword(text2) {
324
- return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
353
+ function visibilityKeyword(text3) {
354
+ return VISIBILITY_KEYWORDS.has(text3) ? text3 : null;
325
355
  }
326
356
  function setVisibility(visibility, name, keyword) {
327
357
  if (keyword === "public") {
@@ -521,6 +551,51 @@ function readCallArgs(argumentList) {
521
551
  }
522
552
  return { positional, keyword };
523
553
  }
554
+ var CONTROL_OPERATORS = /* @__PURE__ */ new Set([
555
+ "and",
556
+ "or",
557
+ "&&",
558
+ "||"
559
+ ]);
560
+ var HANDS_ON = /* @__PURE__ */ new Set(["return", "next", "break"]);
561
+ var isChild = (fieldName) => (parent, child) => field(parent, fieldName)?.id === child.id;
562
+ var always = () => true;
563
+ var asCondition = isChild("condition");
564
+ var VALUE_USED_UNDER = {
565
+ assignment: isChild("right"),
566
+ operator_assignment: isChild("right"),
567
+ call: isChild("receiver"),
568
+ element_reference: always,
569
+ argument_list: (parent) => !HANDS_ON.has(parent.parent?.type ?? ""),
570
+ pair: always,
571
+ array: always,
572
+ hash: always,
573
+ interpolation: always,
574
+ unary: always,
575
+ binary: (parent) => !CONTROL_OPERATORS.has(field(parent, "operator")?.text ?? ""),
576
+ if: asCondition,
577
+ unless: asCondition,
578
+ while: asCondition,
579
+ until: asCondition,
580
+ if_modifier: asCondition,
581
+ unless_modifier: asCondition,
582
+ while_modifier: asCondition,
583
+ until_modifier: asCondition,
584
+ conditional: asCondition,
585
+ case: isChild("value")
586
+ };
587
+ function resultUsedAsValue(node) {
588
+ let child = node;
589
+ let parent = node.parent;
590
+ while (parent !== null && parent.type === "parenthesized_statements" && parent.namedChildCount === 1) {
591
+ child = parent;
592
+ parent = parent.parent;
593
+ }
594
+ if (parent === null) {
595
+ return false;
596
+ }
597
+ return VALUE_USED_UNDER[parent.type]?.(parent, child) ?? false;
598
+ }
524
599
  var NodeSet = class extends import_extractor.IdSet {
525
600
  };
526
601
  var NodeMap = class extends import_extractor.IdMap {
@@ -546,8 +621,8 @@ function lowerAcronyms(word, acronyms) {
546
621
  (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
547
622
  );
548
623
  }
549
- function escapeRegExp(text2) {
550
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
624
+ function escapeRegExp(text3) {
625
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
551
626
  }
552
627
  var PATH_CONVENTIONS = {
553
628
  railsUnderscore: underscoreConstantPath
@@ -555,19 +630,23 @@ var PATH_CONVENTIONS = {
555
630
  function createConstantFileCache() {
556
631
  return { autoloadDirectories: /* @__PURE__ */ new Map(), resolved: /* @__PURE__ */ new Map() };
557
632
  }
558
- function resolveConstantFile(root, qualifiedName, convention, acronyms = [], cache = createConstantFileCache()) {
633
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = [], cache = createConstantFileCache(), autoloadRoots = []) {
559
634
  const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
560
- const key = `${root}\0${relative}`;
635
+ const key = [root, ...autoloadRoots, relative].join("\0");
561
636
  const known = cache.resolved.get(key);
562
637
  if (known !== void 0) {
563
638
  return known;
564
639
  }
565
- const found = firstExisting(root, relative, cache);
640
+ const found = firstExisting(root, relative, cache, autoloadRoots);
566
641
  cache.resolved.set(key, found);
567
642
  return found;
568
643
  }
569
- function firstExisting(root, relative, cache) {
570
- for (const candidate of [root, ...autoloadDirectoriesOf(root, cache)]) {
644
+ function firstExisting(root, relative, cache, autoloadRoots) {
645
+ for (const candidate of [
646
+ root,
647
+ ...autoloadDirectoriesOf(root, cache),
648
+ ...autoloadRoots
649
+ ]) {
571
650
  const file = import_node_path.default.join(candidate, relative);
572
651
  if (import_node_fs.default.existsSync(file)) {
573
652
  return file;
@@ -614,6 +693,7 @@ var import_resolution5 = require("@suss/resolution");
614
693
  var import_values2 = require("@suss/values");
615
694
 
616
695
  // src/facts/resolve.ts
696
+ var import_behavioral_ir = require("@suss/behavioral-ir");
617
697
  var import_datalog = require("@suss/datalog");
618
698
  var import_resolution = require("@suss/resolution");
619
699
 
@@ -630,6 +710,80 @@ var CONVERTING_FUNCTIONS = /* @__PURE__ */ new Set([
630
710
  "String",
631
711
  "Float"
632
712
  ]);
713
+ var OBJECT_METHODS = /* @__PURE__ */ new Set([
714
+ "Array",
715
+ "Float",
716
+ "Hash",
717
+ "Integer",
718
+ "String",
719
+ "abort",
720
+ "binding",
721
+ "block_given?",
722
+ "caller",
723
+ "catch",
724
+ "class",
725
+ "clone",
726
+ "define_singleton_method",
727
+ "dup",
728
+ "eql?",
729
+ "equal?",
730
+ "eval",
731
+ "exit",
732
+ "extend",
733
+ "fail",
734
+ "format",
735
+ "freeze",
736
+ "frozen?",
737
+ "hash",
738
+ "inspect",
739
+ "instance_of?",
740
+ "instance_variable_defined?",
741
+ "instance_variable_get",
742
+ "instance_variable_set",
743
+ "instance_variables",
744
+ "is_a?",
745
+ "itself",
746
+ "kind_of?",
747
+ "lambda",
748
+ "loop",
749
+ "method",
750
+ "methods",
751
+ "nil?",
752
+ "object_id",
753
+ "p",
754
+ "pp",
755
+ "print",
756
+ "printf",
757
+ "proc",
758
+ "public_send",
759
+ "puts",
760
+ "raise",
761
+ "rand",
762
+ "require",
763
+ "require_relative",
764
+ "respond_to?",
765
+ "send",
766
+ "singleton_class",
767
+ "sleep",
768
+ "sprintf",
769
+ "srand",
770
+ "system",
771
+ "tap",
772
+ "then",
773
+ "throw",
774
+ "to_s",
775
+ "warn",
776
+ "yield_self"
777
+ ]);
778
+ var SENDING_METHODS = /* @__PURE__ */ new Set([
779
+ "send",
780
+ "public_send",
781
+ "__send__"
782
+ ]);
783
+ var METHOD_REFERENCE = "method";
784
+ var INVOKE_METHOD = "call";
785
+ var BLOCK_WRAPPERS = /* @__PURE__ */ new Set(["proc", "lambda"]);
786
+ var PROC_CLASS = "Proc";
633
787
  var CONSTRUCTOR = "new";
634
788
 
635
789
  // src/facts/resolve.ts
@@ -677,7 +831,13 @@ function resolveEnvObjects(db, objects) {
677
831
  }
678
832
  function writtenValueOf(db, key) {
679
833
  resolveValues(db, [key]);
680
- return (0, import_resolution.writtenValueOf)(db, key, (keys) => resolveValues(db, keys));
834
+ return (0, import_resolution.writtenValueOf)(
835
+ db,
836
+ key,
837
+ (keys) => resolveValues(db, keys),
838
+ void 0,
839
+ (0, import_resolution.answersFromTestsLeftOut)(key, import_behavioral_ir.isTestFile)
840
+ );
681
841
  }
682
842
  function writtenValueUnder(db, key, site) {
683
843
  const program = (0, import_resolution.resolutionUnderProgram)(RUBY_RULES);
@@ -709,7 +869,7 @@ function isFieldOf(parent, node, name) {
709
869
  function namedAt(parent, node) {
710
870
  return isFieldOf(parent, node, "name");
711
871
  }
712
- function always() {
872
+ function always2() {
713
873
  return true;
714
874
  }
715
875
  var SPELLS_A_NAME = {
@@ -724,14 +884,14 @@ var SPELLS_A_NAME = {
724
884
  hash_splat_parameter: namedAt,
725
885
  keyword_parameter: namedAt,
726
886
  block_parameter: namedAt,
727
- destructured_parameter: always,
728
- left_assignment_list: always,
729
- rest_assignment: always,
730
- destructured_left_assignment: always,
731
- exception_variable: always,
732
- undef: always,
733
- alias: always,
734
- ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always]))
887
+ destructured_parameter: always2,
888
+ left_assignment_list: always2,
889
+ rest_assignment: always2,
890
+ destructured_left_assignment: always2,
891
+ exception_variable: always2,
892
+ undef: always2,
893
+ alias: always2,
894
+ ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always2]))
735
895
  };
736
896
  function addNamesUnder(node, names) {
737
897
  if (node === null) {
@@ -1225,6 +1385,9 @@ function add(emitter, relation, ...tuple) {
1225
1385
  }
1226
1386
  emitter.db.add(relation, tuple);
1227
1387
  }
1388
+ function rowsOf(emitter) {
1389
+ return (relation, row) => add(emitter, relation, ...row);
1390
+ }
1228
1391
  function valueKey(emitter, written, parent) {
1229
1392
  return readKey(emitter.filePath, written, emitter.enclosing, parent);
1230
1393
  }
@@ -1308,6 +1471,97 @@ function emitCall(emitter, site) {
1308
1471
  }
1309
1472
  emitCallArguments(emitter, site);
1310
1473
  emitConversion(emitter, site);
1474
+ emitSentChoice(emitter, site);
1475
+ emitMethodReference(emitter, site);
1476
+ emitBlockAsProc(emitter, site);
1477
+ }
1478
+ function emitBlockAsProc(emitter, site) {
1479
+ const { method, receiver, node } = site;
1480
+ const name = method?.text;
1481
+ const wraps = receiver === null ? name !== void 0 && BLOCK_WRAPPERS.has(name) : name === CONSTRUCTOR && receiver.text === PROC_CLASS;
1482
+ const block = field(node, "block");
1483
+ if (!wraps || block === null) {
1484
+ return;
1485
+ }
1486
+ const blockKey = nodeId(emitter.filePath, block);
1487
+ add(emitter, "func", blockKey);
1488
+ add(emitter, "binds", site.key, blockKey);
1489
+ for (const call of callsWrittenIn(block)) {
1490
+ add(emitter, "makesCall", blockKey, nodeId(emitter.filePath, call));
1491
+ }
1492
+ }
1493
+ function callsWrittenIn(node, found = []) {
1494
+ for (const child of children(node)) {
1495
+ if (OWN_BODY_TYPES.has(child.type)) {
1496
+ continue;
1497
+ }
1498
+ if (child.type === "call") {
1499
+ found.push(child);
1500
+ }
1501
+ callsWrittenIn(child, found);
1502
+ }
1503
+ return found;
1504
+ }
1505
+ function emitSentChoice(emitter, site) {
1506
+ const { method, receiver, node } = site;
1507
+ if (method === null || !SENDING_METHODS.has(method.text)) {
1508
+ return;
1509
+ }
1510
+ const objectKey = receiver === null ? emitter.selfKey : valueKey(emitter, receiver, node);
1511
+ const [name] = readCallArgs(field(node, "arguments")).positional;
1512
+ if (objectKey === null || name === void 0) {
1513
+ return;
1514
+ }
1515
+ add(emitter, "callsChosenEntry", site.key);
1516
+ const written = symbolValue(name) ?? stringLiteralValue(name);
1517
+ if (written !== null) {
1518
+ for (const spelled of namesReadAs(emitter, receiver, objectKey, written)) {
1519
+ add(emitter, "choosesNamedEntry", site.key, objectKey, spelled);
1520
+ }
1521
+ return;
1522
+ }
1523
+ const ends = writtenEnds(name);
1524
+ if (ends === null) {
1525
+ add(emitter, "choosesEntry", site.key, objectKey);
1526
+ return;
1527
+ }
1528
+ for (const start of namesReadAs(emitter, receiver, objectKey, ends.start)) {
1529
+ add(emitter, "choosesEntryLike", site.key, objectKey, start, ends.end);
1530
+ }
1531
+ }
1532
+ function writtenEnds(node) {
1533
+ if (node.type !== "string" && node.type !== "delimited_symbol") {
1534
+ return null;
1535
+ }
1536
+ const parts = children(node);
1537
+ const written = (part) => part.type === "string_content";
1538
+ const firstComputed = parts.findIndex((part) => !written(part));
1539
+ if (firstComputed === -1) {
1540
+ return null;
1541
+ }
1542
+ const lastComputed = parts.length - 1 - [...parts].reverse().findIndex((part) => !written(part));
1543
+ const start = parts.slice(0, firstComputed).map((part) => part.text).join("");
1544
+ const end = parts.slice(lastComputed + 1).map((part) => part.text).join("");
1545
+ return start === "" && end === "" ? null : { start, end };
1546
+ }
1547
+ function emitMethodReference(emitter, site) {
1548
+ const { method, receiver, node } = site;
1549
+ if (method?.text !== METHOD_REFERENCE || receiver !== null || emitter.selfKey === null) {
1550
+ return;
1551
+ }
1552
+ const [name] = readCallArgs(field(node, "arguments")).positional;
1553
+ const written = name === void 0 ? null : symbolValue(name) ?? stringLiteralValue(name);
1554
+ if (written === null) {
1555
+ return;
1556
+ }
1557
+ for (const spelled of namesReadAs(
1558
+ emitter,
1559
+ receiver,
1560
+ emitter.selfKey,
1561
+ written
1562
+ )) {
1563
+ add(emitter, "readsProperty", site.key, emitter.selfKey, spelled);
1564
+ }
1311
1565
  }
1312
1566
  function emitConversion(emitter, site) {
1313
1567
  const { method, receiver, node } = site;
@@ -1381,7 +1635,6 @@ var SELF_READS = {
1381
1635
  function classSpelling(name) {
1382
1636
  return name === CONSTRUCTOR ? name : (0, import_resolution4.classMemberName)(name);
1383
1637
  }
1384
- var INVOKE_METHOD = "call";
1385
1638
  function invokedValueOf(call) {
1386
1639
  if (call.type !== "call") {
1387
1640
  return null;
@@ -1427,6 +1680,7 @@ function emitArray(emitter, array) {
1427
1680
  const objectKey = nodeId(emitter.filePath, array);
1428
1681
  add(emitter, "objectValue", objectKey);
1429
1682
  let position = 0;
1683
+ const entries = [];
1430
1684
  for (const element of children(array)) {
1431
1685
  add(
1432
1686
  emitter,
@@ -1436,12 +1690,24 @@ function emitArray(emitter, array) {
1436
1690
  valueKey(emitter, element, array)
1437
1691
  );
1438
1692
  position += 1;
1693
+ entries.push(writtenEntry(emitter, element, array));
1439
1694
  }
1695
+ (0, import_resolution4.addEntryRows)(rowsOf(emitter), objectKey, entries);
1696
+ }
1697
+ var SPREAD_TYPES = /* @__PURE__ */ new Set(["splat_argument", "hash_splat_argument"]);
1698
+ function writtenEntry(emitter, entry, parent) {
1699
+ const unpacked = SPREAD_TYPES.has(entry.type) ? children(entry)[0] : void 0;
1700
+ return unpacked === void 0 ? { key: valueKey(emitter, entry, parent), form: import_resolution4.VALUE_ENTRY } : { key: valueKey(emitter, unpacked, entry), form: import_resolution4.SPREAD_ENTRY };
1440
1701
  }
1441
1702
  function emitHash(emitter, hash) {
1442
1703
  const objectKey = nodeId(emitter.filePath, hash);
1443
1704
  add(emitter, "objectValue", objectKey);
1705
+ const entries = [];
1444
1706
  for (const pair of children(hash)) {
1707
+ if (SPREAD_TYPES.has(pair.type)) {
1708
+ entries.push(writtenEntry(emitter, pair, hash));
1709
+ continue;
1710
+ }
1445
1711
  if (pair.type !== "pair") {
1446
1712
  continue;
1447
1713
  }
@@ -1450,6 +1716,7 @@ function emitHash(emitter, hash) {
1450
1716
  if (key === null || value === null) {
1451
1717
  continue;
1452
1718
  }
1719
+ entries.push(writtenEntry(emitter, value, pair));
1453
1720
  const keyText = pairKeyText(key);
1454
1721
  if (keyText === null) {
1455
1722
  add(emitter, "holdsUnderKey", objectKey, valueKey(emitter, value, pair));
@@ -1463,32 +1730,66 @@ function emitHash(emitter, hash) {
1463
1730
  valueKey(emitter, value, pair)
1464
1731
  );
1465
1732
  }
1733
+ (0, import_resolution4.addEntryRows)(rowsOf(emitter), objectKey, entries);
1466
1734
  }
1467
1735
  var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1468
1736
  var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
1469
- var DEFINE_METHOD_CALL = "define_method";
1470
- function loopTurnAt(emitter, block, call) {
1471
- if (call.type !== "call") {
1472
- return null;
1473
- }
1474
- const receiver = field(call, "receiver");
1475
- const method = field(call, "method");
1737
+ var COUNTING_LOOP = "each_with_index";
1738
+ function emitLoopVariables(emitter, block, call) {
1739
+ const receiver = call.type === "call" ? field(call, "receiver") : null;
1740
+ const method = receiver === null ? null : field(call, "method");
1476
1741
  if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
1477
- return null;
1742
+ return;
1478
1743
  }
1479
- const parameters = field(block, "parameters");
1480
- const names = parameters === null ? [] : children(parameters).map((p) => p.text);
1481
- const element = names[0];
1482
- if (element === void 0) {
1483
- return null;
1744
+ const variables = children(field(block, "parameters")).filter(
1745
+ (variable) => variable.type !== "block_parameter"
1746
+ );
1747
+ const counting = method.text === COUNTING_LOOP;
1748
+ const [first, position] = variables;
1749
+ if (first === void 0 || counting && variables.length > 2) {
1750
+ return;
1484
1751
  }
1485
- return {
1486
- element,
1487
- index: names[1] ?? "",
1488
- overKey: nodeId(emitter.filePath, receiver)
1489
- };
1752
+ const taken = counting || variables.length === 1 ? elementTakenBy(first) : partsTakenBy(variables);
1753
+ const blockKey = nodeId(emitter.filePath, block);
1754
+ const over = valueKey(emitter, receiver, call);
1755
+ for (const variable of taken) {
1756
+ const key = `${blockKey}#${variable.name}`;
1757
+ if (variable.part === null) {
1758
+ add(emitter, "elementOf", key, over);
1759
+ continue;
1760
+ }
1761
+ add(emitter, "elementPartOf", key, over, variable.part);
1762
+ }
1763
+ const [anchor] = taken;
1764
+ if (counting && position?.type === "identifier" && anchor !== void 0) {
1765
+ add(
1766
+ emitter,
1767
+ "positionOf",
1768
+ `${blockKey}#${position.text}`,
1769
+ `${blockKey}#${anchor.name}`
1770
+ );
1771
+ }
1772
+ }
1773
+ function elementTakenBy(variable) {
1774
+ if (variable.type === "identifier") {
1775
+ return [{ name: variable.text, part: null }];
1776
+ }
1777
+ return variable.type === "destructured_parameter" ? partsTakenBy(children(variable)) : [];
1778
+ }
1779
+ function partsTakenBy(variables) {
1780
+ const parts = [];
1781
+ for (const [position, variable] of variables.entries()) {
1782
+ if (variable.type === "splat_parameter") {
1783
+ break;
1784
+ }
1785
+ if (variable.type === "identifier") {
1786
+ parts.push({ name: variable.text, part: String(position) });
1787
+ }
1788
+ }
1789
+ return parts;
1490
1790
  }
1491
- function emitDynamicDefinition(emitter, call, type, turns) {
1791
+ var DEFINE_METHOD_CALL = "define_method";
1792
+ function emitDynamicDefinition(emitter, call, type) {
1492
1793
  if (emitter.selfKey === null || // A call inside a method runs when the method runs, not when the class loads.
1493
1794
  emitter.enclosing !== null || type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
1494
1795
  return;
@@ -1497,33 +1798,16 @@ function emitDynamicDefinition(emitter, call, type, turns) {
1497
1798
  const first = args === null ? void 0 : children(args)[0];
1498
1799
  const nameKey2 = nodeId(emitter.filePath, first ?? call);
1499
1800
  add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
1500
- for (const turn of turns) {
1501
- add(
1502
- emitter,
1503
- "nameTurnsOn",
1504
- nameKey2,
1505
- turn.element,
1506
- turn.index,
1507
- turn.overKey
1508
- );
1509
- }
1510
1801
  }
1511
- function walkExpressions(node, emitter, visit, nested = null, turns = []) {
1802
+ function walkExpressions(node, visit, nested = null) {
1512
1803
  for (const child of children(node)) {
1513
1804
  const type = child.type;
1514
1805
  if (OWN_BODY_TYPES.has(type)) {
1515
1806
  nested?.push(child);
1516
1807
  continue;
1517
1808
  }
1518
- visit(child, type, node, turns);
1519
- const opened = BLOCK_TYPES3.has(type) ? loopTurnAt(emitter, child, node) : null;
1520
- walkExpressions(
1521
- child,
1522
- emitter,
1523
- visit,
1524
- nested,
1525
- opened === null ? turns : [...turns, opened]
1526
- );
1809
+ visit(child, type, node);
1810
+ walkExpressions(child, visit, nested);
1527
1811
  }
1528
1812
  }
1529
1813
  var WRITTEN_KEY_TYPES = /* @__PURE__ */ new Set([
@@ -1537,22 +1821,23 @@ function emitKeyedRead(emitter, site, container, key, keyParent) {
1537
1821
  if (WRITTEN_KEY_TYPES.has(key.type)) {
1538
1822
  return;
1539
1823
  }
1824
+ const siteKey = nodeId(emitter.filePath, site);
1825
+ const containerKey = valueKey(emitter, container, site);
1540
1826
  add(
1541
1827
  emitter,
1542
1828
  "readsKeyed",
1543
- nodeId(emitter.filePath, site),
1544
- valueKey(emitter, container, site),
1829
+ siteKey,
1830
+ containerKey,
1545
1831
  valueKey(emitter, key, keyParent)
1546
1832
  );
1833
+ add(emitter, "choosesEntry", siteKey, containerKey);
1547
1834
  }
1548
1835
  function emitKeyedElement(emitter, node, parent) {
1549
1836
  const object = field(node, "object");
1550
1837
  if (object === null || isWriteTarget(node, parent)) {
1551
1838
  return;
1552
1839
  }
1553
- const index = node.namedChildren.find(
1554
- (child) => child !== null && child.id !== object.id
1555
- );
1840
+ const index = children(node).find((child) => child.id !== object.id);
1556
1841
  if (index !== void 0) {
1557
1842
  emitKeyedRead(emitter, node, object, index, node);
1558
1843
  emitWrittenKeyRead(emitter, node, object, index);
@@ -1593,27 +1878,27 @@ function fallbackBranchesOf(node, type, enclosing) {
1593
1878
  }
1594
1879
  return [left, right].filter((side) => !raises(side, enclosing));
1595
1880
  }
1596
- var RAISING_METHODS = /* @__PURE__ */ new Set(["raise", "fail"]);
1881
+ var RAISING_METHODS2 = /* @__PURE__ */ new Set(["raise", "fail"]);
1597
1882
  function raises(written, enclosing) {
1598
1883
  const node = readThrough(written);
1599
1884
  if (node.type === "identifier") {
1600
- return RAISING_METHODS.has(node.text) && isBareCall(node, enclosing);
1885
+ return RAISING_METHODS2.has(node.text) && isBareCall(node, enclosing);
1601
1886
  }
1602
1887
  if (node.type !== "call") {
1603
1888
  return false;
1604
1889
  }
1605
1890
  const receiver = field(node, "receiver");
1606
- return RAISING_METHODS.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1891
+ return RAISING_METHODS2.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1607
1892
  }
1608
1893
  function emitExpressionFacts(emitter, node) {
1609
- emitDynamicDefinition(emitter, node, node.type, []);
1610
- walkExpressions(node, emitter, (child, type, parent, turns) => {
1894
+ emitDynamicDefinition(emitter, node, node.type);
1895
+ walkExpressions(node, (child, type, parent) => {
1611
1896
  const site = callSiteAt(emitter, child, type, parent);
1612
- emitExpressionFact(emitter, child, type, parent, turns, site);
1897
+ emitExpressionFact(emitter, child, type, parent, site);
1613
1898
  });
1614
1899
  }
1615
- function emitExpressionFact(emitter, child, type, parent, turns, site) {
1616
- emitDynamicDefinition(emitter, child, type, turns);
1900
+ function emitExpressionFact(emitter, child, type, parent, site) {
1901
+ emitDynamicDefinition(emitter, child, type);
1617
1902
  if (site !== null) {
1618
1903
  emitCall(emitter, site);
1619
1904
  emitKeyedFetch(emitter, site);
@@ -1654,6 +1939,11 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1654
1939
  }
1655
1940
  if (BLOCK_TYPES3.has(type)) {
1656
1941
  emitBlockValue(emitter, child);
1942
+ emitLoopVariables(emitter, child, parent);
1943
+ }
1944
+ const pieces = TEMPLATE_PIECES[type]?.(emitter, child) ?? [];
1945
+ if (pieces.length > 0) {
1946
+ (0, import_resolution4.addTemplateRows)(rowsOf(emitter), nodeId(emitter.filePath, child), pieces);
1657
1947
  }
1658
1948
  for (const branch of BRANCHES_OF[type]?.(child) ?? []) {
1659
1949
  add(
@@ -1673,6 +1963,29 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1673
1963
  emitParameterDefault(emitter, child);
1674
1964
  }
1675
1965
  }
1966
+ function interpolatedPieces(emitter, string) {
1967
+ const parts = children(string);
1968
+ if (!parts.some((part) => part.type === "interpolation")) {
1969
+ return [];
1970
+ }
1971
+ return parts.flatMap((part) => {
1972
+ if (part.type === "string_content") {
1973
+ return [{ text: part.text }];
1974
+ }
1975
+ const expression = part.type === "interpolation" ? children(part)[0] : void 0;
1976
+ return expression === void 0 ? [] : [{ hole: valueKey(emitter, expression, part) }];
1977
+ });
1978
+ }
1979
+ function adjacentPieces(emitter, joined2) {
1980
+ return children(joined2).map((part) => ({
1981
+ hole: valueKey(emitter, part, joined2)
1982
+ }));
1983
+ }
1984
+ var TEMPLATE_PIECES = {
1985
+ string: interpolatedPieces,
1986
+ delimited_symbol: interpolatedPieces,
1987
+ chained_string: adjacentPieces
1988
+ };
1676
1989
  function emitBlockArgument(emitter, argument) {
1677
1990
  const [handed] = children(argument);
1678
1991
  if (handed !== void 0) {
@@ -2095,11 +2408,10 @@ function emitMethodFacts(emitter, method) {
2095
2408
  const collecting = { ...inside, heldRows: expressionFacts };
2096
2409
  walkExpressions(
2097
2410
  body,
2098
- inside,
2099
- (child, type, parent, turns) => {
2411
+ (child, type, parent) => {
2100
2412
  const site = callSiteAt(inside, child, type, parent);
2101
2413
  emitBodyFact(bodyFacts, inside, funcKey, child, type, site);
2102
- emitExpressionFact(collecting, child, type, parent, turns, site);
2414
+ emitExpressionFact(collecting, child, type, parent, site);
2103
2415
  },
2104
2416
  nested
2105
2417
  );
@@ -2559,9 +2871,7 @@ var EXPRESSION_TYPES = {
2559
2871
  ),
2560
2872
  unary: (node) => {
2561
2873
  const operand2 = field(node, "operand");
2562
- const operator = node.children.find(
2563
- (child) => child !== null && !child.isNamed
2564
- );
2874
+ const operator = allChildren(node).find((child) => !child.isNamed);
2565
2875
  if (operand2 === null || operator === void 0) {
2566
2876
  return OPAQUE;
2567
2877
  }
@@ -2763,8 +3073,25 @@ function statementOf(node) {
2763
3073
  if (appending !== null) {
2764
3074
  return appending;
2765
3075
  }
3076
+ if (isRaiseCall(node) && !rescuedInSameFunction(node)) {
3077
+ return { kind: "throw" };
3078
+ }
2766
3079
  return isTailExpression(node) ? { kind: "return", value: node } : { kind: "expression", value: node };
2767
3080
  }
3081
+ var RESCUING_TYPES = /* @__PURE__ */ new Set(["begin", "body_statement"]);
3082
+ var UNRESCUED_PARTS = /* @__PURE__ */ new Set(["rescue", "else", "ensure"]);
3083
+ function rescuedInSameFunction(node) {
3084
+ let child = node;
3085
+ let parent = node.parent;
3086
+ while (parent !== null && !isRoot(parent)) {
3087
+ if (RESCUING_TYPES.has(parent.type) && !UNRESCUED_PARTS.has(child.type) && children(parent).some((part) => part.type === "rescue")) {
3088
+ return true;
3089
+ }
3090
+ child = parent;
3091
+ parent = parent.parent;
3092
+ }
3093
+ return false;
3094
+ }
2768
3095
  function appendingStatement(node) {
2769
3096
  if (node.type !== "binary" || field(node, "operator")?.text !== "<<") {
2770
3097
  return null;
@@ -3420,6 +3747,10 @@ function writtenNodeOf(node, db, site) {
3420
3747
  const context = db === void 0 ? void 0 : contexts.get(db);
3421
3748
  return context === void 0 ? null : context.writtenTo(node, site);
3422
3749
  }
3750
+ function calledDefinition(call, db) {
3751
+ const context = db === void 0 ? void 0 : contexts.get(db);
3752
+ return context === void 0 ? null : context.callable(call);
3753
+ }
3423
3754
  function askWrittenValues(nodes, db) {
3424
3755
  if (db === void 0 || !contexts.has(db)) {
3425
3756
  return;
@@ -3482,7 +3813,7 @@ function methodDefinitionsIn(file, root) {
3482
3813
  if (METHOD_TYPES.has(node.type)) {
3483
3814
  found.set(nodeId(file, node), node);
3484
3815
  }
3485
- for (const child of node.namedChildren) {
3816
+ for (const child of children(node)) {
3486
3817
  if (child !== null) {
3487
3818
  visit(child);
3488
3819
  }
@@ -3560,8 +3891,24 @@ function literalElementsIn(value) {
3560
3891
  }
3561
3892
  return elements;
3562
3893
  }
3563
- function literalElementsOf(node, db) {
3564
- return literalElementsIn(evaluatedValue(node, db));
3894
+ function loopElementsOf(node, db) {
3895
+ const value = evaluatedValue(node, db);
3896
+ if (value.kind === "sequence") {
3897
+ return value.items.map((item) => elementOfList((0, import_values4.force)(item.value)));
3898
+ }
3899
+ if (value.kind === "record" && !value.open) {
3900
+ return [...value.fields].map(([key, item]) => {
3901
+ const parts = [(0, import_values4.text)(key), (0, import_values4.force)(item.value)];
3902
+ return { whole: (0, import_values4.sequence)(parts), parts };
3903
+ });
3904
+ }
3905
+ return null;
3906
+ }
3907
+ function elementOfList(whole) {
3908
+ return {
3909
+ whole,
3910
+ parts: whole.kind === "sequence" ? whole.items.map((item) => (0, import_values4.force)(item.value)) : [whole]
3911
+ };
3565
3912
  }
3566
3913
  function namesOf(node, db) {
3567
3914
  const value = evaluatedValue(node, db);
@@ -3583,13 +3930,16 @@ function definedNamesOf(dynamic, classKey) {
3583
3930
  }
3584
3931
  function readDynamicNames(db, rootsByFile) {
3585
3932
  const byClass = /* @__PURE__ */ new Map();
3586
- for (const [classKey, nameKey2] of db.facts("definesMethodFrom")) {
3933
+ const definitions = db.facts("definesMethodFrom");
3934
+ const loops = definitions.length === 0 ? /* @__PURE__ */ new Map() : loopsByFile(db);
3935
+ for (const [classKey, nameKey2] of definitions) {
3587
3936
  const key = String(classKey);
3937
+ const around = loopsAround(loops, key, String(nameKey2));
3588
3938
  byClass.set(
3589
3939
  key,
3590
3940
  foldReading(
3591
3941
  byClass.get(key) ?? NOTHING,
3592
- readOne(db, rootsByFile, String(nameKey2))
3942
+ readOne(db, rootsByFile, String(nameKey2), around)
3593
3943
  )
3594
3944
  );
3595
3945
  }
@@ -3610,11 +3960,11 @@ function foldReading(soFar, reading) {
3610
3960
  unreadable: soFar.unreadable
3611
3961
  };
3612
3962
  }
3613
- function readOne(db, rootsByFile, nameKey2) {
3963
+ function readOne(db, rootsByFile, nameKey2, loops) {
3614
3964
  const node = (0, import_resolution6.nodeOfKey)(rootsByFile, nameKey2);
3615
3965
  const names = [];
3616
3966
  const patterns = [];
3617
- for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
3967
+ for (const bindings of loopTurns(db, rootsByFile, loops)) {
3618
3968
  const value = node === null ? (0, import_values5.hole)(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
3619
3969
  const name = (0, import_values5.literalOf)(value);
3620
3970
  if (name !== null) {
@@ -3647,38 +3997,105 @@ function patternOf(value) {
3647
3997
  }
3648
3998
  return readSomething ? new RegExp(`${source}$`) : null;
3649
3999
  }
3650
- function escapeForPattern(text2) {
3651
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4000
+ function escapeForPattern(text3) {
4001
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3652
4002
  }
3653
- function loopTurns(db, rootsByFile, nameKey2) {
4003
+ function loopTurns(db, rootsByFile, loops) {
3654
4004
  let turns = [/* @__PURE__ */ new Map()];
3655
- for (const over of listsBehind(db, rootsByFile, nameKey2)) {
4005
+ for (const loop of loops) {
4006
+ const bindings = bindingsOf(db, rootsByFile, loop);
4007
+ if (bindings === null) {
4008
+ continue;
4009
+ }
3656
4010
  turns = turns.flatMap(
3657
- (base) => over.elements.map((value, position) => {
3658
- const binding = new Map([...base, [over.element, value]]);
3659
- if (over.index !== "") {
3660
- binding.set(over.index, String(position));
3661
- }
3662
- return binding;
3663
- })
4011
+ (base) => bindings.map((binding) => new Map([...base, ...binding]))
3664
4012
  );
3665
4013
  }
3666
4014
  return turns;
3667
4015
  }
3668
- function listsBehind(db, rootsByFile, nameKey2) {
3669
- const found = [];
3670
- for (const [, element, index, overKey] of db.lookup(
3671
- "nameTurnsOn",
3672
- 0,
3673
- nameKey2
3674
- )) {
3675
- const node = (0, import_resolution6.nodeOfKey)(rootsByFile, String(overKey));
3676
- const elements = node === null ? null : literalElementsOf(node, db);
3677
- if (elements !== null) {
3678
- found.push({ element: String(element), index: String(index), elements });
4016
+ function bindingsOf(db, rootsByFile, loop) {
4017
+ const node = collectionNode(rootsByFile, loop);
4018
+ const elements = node === null ? null : loopElementsOf(node, db);
4019
+ if (elements === null) {
4020
+ return null;
4021
+ }
4022
+ const bound = [...loop.variables].flatMap(([name, takes]) => {
4023
+ const values = elements.map(
4024
+ (element, position) => GIVEN[takes.kind](element, position, takes)
4025
+ );
4026
+ return values.every((value) => value !== null) ? [{ name, values }] : [];
4027
+ });
4028
+ if (bound.length === 0) {
4029
+ return null;
4030
+ }
4031
+ return elements.map(
4032
+ (_, position) => new Map(bound.map(({ name, values }) => [name, values[position] ?? ""]))
4033
+ );
4034
+ }
4035
+ var GIVEN = {
4036
+ element: (element) => (0, import_values5.literalOf)(element.whole),
4037
+ part: (element, _, takes) => {
4038
+ const part = takes.kind === "part" ? element.parts[takes.part] : void 0;
4039
+ return part === void 0 ? null : (0, import_values5.literalOf)(part);
4040
+ },
4041
+ position: (_, position) => String(position)
4042
+ };
4043
+ function collectionNode(rootsByFile, loop) {
4044
+ const written = (0, import_resolution6.nodeOfKey)(rootsByFile, loop.over);
4045
+ if (written !== null) {
4046
+ return written;
4047
+ }
4048
+ const call = (0, import_resolution6.nodeOfKey)(rootsByFile, loop.block.key)?.parent ?? null;
4049
+ return call === null ? null : field(call, "receiver");
4050
+ }
4051
+ function blockVariableOf(key) {
4052
+ const at = key.lastIndexOf("#");
4053
+ const span = at < 0 ? null : (0, import_resolution6.spanOfKey)(key.slice(0, at));
4054
+ return span === null ? null : { block: { ...span, key: key.slice(0, at) }, name: key.slice(at + 1) };
4055
+ }
4056
+ function loopsByFile(db) {
4057
+ const blocks = /* @__PURE__ */ new Map();
4058
+ const note = (key, over, takes) => {
4059
+ const variable = blockVariableOf(String(key));
4060
+ if (variable === null) {
4061
+ return;
3679
4062
  }
4063
+ const known = blocks.get(variable.block.key);
4064
+ const loop = known ?? {
4065
+ block: variable.block,
4066
+ over: String(over),
4067
+ variables: /* @__PURE__ */ new Map()
4068
+ };
4069
+ loop.variables.set(variable.name, takes);
4070
+ blocks.set(variable.block.key, loop);
4071
+ };
4072
+ for (const [element, over] of db.facts("elementOf")) {
4073
+ note(element, over, { kind: "element" });
3680
4074
  }
3681
- return found;
4075
+ for (const [element, over, part] of db.facts("elementPartOf")) {
4076
+ note(element, over, { kind: "part", part: Number(part) });
4077
+ }
4078
+ for (const [position, element] of db.facts("positionOf")) {
4079
+ const block = blockVariableOf(String(element))?.block.key;
4080
+ if (block !== void 0 && blocks.has(block)) {
4081
+ note(position, blocks.get(block)?.over, { kind: "position" });
4082
+ }
4083
+ }
4084
+ const byFile = /* @__PURE__ */ new Map();
4085
+ for (const loop of blocks.values()) {
4086
+ byFile.set(loop.block.file, [...byFile.get(loop.block.file) ?? [], loop]);
4087
+ }
4088
+ return byFile;
4089
+ }
4090
+ function loopsAround(loops, classKey, nameKey2) {
4091
+ const cls = (0, import_resolution6.spanOfKey)(classKey);
4092
+ const name = (0, import_resolution6.spanOfKey)(nameKey2);
4093
+ if (cls === null || name === null) {
4094
+ return [];
4095
+ }
4096
+ return (loops.get(name.file) ?? []).filter(
4097
+ ({ block }) => cls.start <= block.start && block.end <= cls.end && block.start <= name.start && name.end <= block.end
4098
+ ).sort((a, b) => a.block.start - b.block.start);
3682
4099
  }
3683
4100
 
3684
4101
  // src/scope.ts
@@ -3808,12 +4225,27 @@ function graphqlTypeNameFromQualified(qualifiedName, convention) {
3808
4225
 
3809
4226
  // src/ancestry.ts
3810
4227
  async function reachDefinition(qualifiedName, lookup) {
4228
+ const segments = qualifiedName.split("::");
4229
+ for (let length = segments.length; length > 0; length--) {
4230
+ const found = await definedInFileOf(
4231
+ segments.slice(0, length).join("::"),
4232
+ qualifiedName,
4233
+ lookup
4234
+ );
4235
+ if (found !== null) {
4236
+ return found;
4237
+ }
4238
+ }
4239
+ return null;
4240
+ }
4241
+ async function definedInFileOf(fileConstant, qualifiedName, lookup) {
3811
4242
  const filePath = resolveConstantFile(
3812
4243
  lookup.root,
3813
- qualifiedName,
4244
+ fileConstant,
3814
4245
  lookup.pathConvention,
3815
4246
  lookup.acronyms,
3816
- lookup.constantFiles
4247
+ lookup.constantFiles,
4248
+ lookup.autoloadRoots
3817
4249
  );
3818
4250
  if (filePath === null) {
3819
4251
  return null;
@@ -3958,7 +4390,7 @@ function superclassCandidatesOf(blocks) {
3958
4390
  return [];
3959
4391
  }
3960
4392
  function methodInAncestry(ancestry, name, read = {}) {
3961
- for (const entry of ancestry) {
4393
+ for (const [index, entry] of ancestry.entries()) {
3962
4394
  if (entry.type === "root") {
3963
4395
  return { type: "none" };
3964
4396
  }
@@ -3966,7 +4398,8 @@ function methodInAncestry(ancestry, name, read = {}) {
3966
4398
  return {
3967
4399
  type: "unsettled",
3968
4400
  reason: `inherited from ${entry.name}, which this run did not read`,
3969
- cause: "unreadAncestor"
4401
+ cause: "unreadAncestor",
4402
+ definedLater: definedInProjectAfter(ancestry, index, name, read)
3970
4403
  };
3971
4404
  }
3972
4405
  const found = definitionIn(entry.blocks, name, read);
@@ -3983,6 +4416,18 @@ function methodInAncestry(ancestry, name, read = {}) {
3983
4416
  }
3984
4417
  return { type: "none" };
3985
4418
  }
4419
+ function definedInProjectAfter(ancestry, index, name, read) {
4420
+ return ancestry.slice(index + 1).some(
4421
+ (entry) => entry.type !== "root" && entry.type !== "unfollowed" && definitionIn(entry.blocks, name, read).method !== null
4422
+ );
4423
+ }
4424
+ function methodPastUnreadAncestors(ancestry, name, read = {}) {
4425
+ return methodInAncestry(
4426
+ ancestry.filter((entry) => entry.type !== "unfollowed"),
4427
+ name,
4428
+ read
4429
+ );
4430
+ }
3986
4431
  function definitionIn(blocks, name, read) {
3987
4432
  let method = null;
3988
4433
  let block = null;
@@ -4027,12 +4472,12 @@ function inheritedStatements(ancestry, blockConfigures) {
4027
4472
  }
4028
4473
 
4029
4474
  // src/discovery.ts
4030
- var import_behavioral_ir5 = require("@suss/behavioral-ir");
4031
- var import_extractor9 = require("@suss/extractor");
4475
+ var import_behavioral_ir6 = require("@suss/behavioral-ir");
4476
+ var import_extractor11 = require("@suss/extractor");
4032
4477
 
4033
4478
  // src/clientCalls.ts
4034
- var import_behavioral_ir = require("@suss/behavioral-ir");
4035
- var import_values13 = require("@suss/values");
4479
+ var import_behavioral_ir2 = require("@suss/behavioral-ir");
4480
+ var import_values14 = require("@suss/values");
4036
4481
 
4037
4482
  // src/paths/effects.ts
4038
4483
  var import_extractor2 = require("@suss/extractor");
@@ -4059,7 +4504,7 @@ function isRaise(node) {
4059
4504
  return false;
4060
4505
  }
4061
4506
  const receiver = field(node, "receiver");
4062
- const method = field(node, "method") ?? node.namedChildren[0] ?? null;
4507
+ const method = field(node, "method") ?? node.namedChild(0) ?? null;
4063
4508
  return receiver === null && method !== null && RAISE_NAMES.has(method.text);
4064
4509
  }
4065
4510
  function exitOf(node) {
@@ -4071,7 +4516,7 @@ function exitOf(node) {
4071
4516
  function exitKindOf(node) {
4072
4517
  let sawReturn = false;
4073
4518
  const raises2 = (current) => {
4074
- for (const child of current.namedChildren) {
4519
+ for (const child of children(current)) {
4075
4520
  if (child === null || CAPTURES_RETURN.has(child.type)) {
4076
4521
  continue;
4077
4522
  }
@@ -4105,13 +4550,52 @@ function callHeaderOf(call, block) {
4105
4550
  function blockStatements(block) {
4106
4551
  return block === null ? [] : children(block);
4107
4552
  }
4553
+ function attachedBlockOf(call) {
4554
+ return children(call).find(
4555
+ (child) => child !== null && (child.type === "do_block" || child.type === "block")
4556
+ ) ?? null;
4557
+ }
4558
+ function soleBlockParameter(block) {
4559
+ const names = children(field(block, "parameters")).filter(
4560
+ (child) => child.type === "identifier"
4561
+ );
4562
+ return names.length === 1 ? names[0].text : null;
4563
+ }
4564
+ function blockBodyStatements(block) {
4565
+ const body = field(block, "body");
4566
+ if (body !== null) {
4567
+ return children(body);
4568
+ }
4569
+ return children(block).filter((child) => child.type !== "block_parameters");
4570
+ }
4571
+ function armBody(block) {
4572
+ return field(block, "body") ?? block;
4573
+ }
4574
+ function formatArmOf(statement, parameter) {
4575
+ if (statement.type !== "call") {
4576
+ return null;
4577
+ }
4578
+ const receiver = field(statement, "receiver");
4579
+ const format = field(statement, "method")?.text;
4580
+ if (receiver?.text !== parameter || format === void 0) {
4581
+ return null;
4582
+ }
4583
+ const block = attachedBlockOf(statement);
4584
+ return {
4585
+ format,
4586
+ header: block === null ? statement.text : callHeaderOf(statement, block).sourceText,
4587
+ block
4588
+ };
4589
+ }
4108
4590
  var Lowerer = class {
4109
- constructor(terminals, responseCalls) {
4591
+ constructor(terminals, responseCalls, formatSwitch) {
4110
4592
  this.terminals = terminals;
4111
4593
  this.responseCalls = responseCalls;
4594
+ this.formatSwitch = formatSwitch;
4112
4595
  }
4113
4596
  terminals;
4114
4597
  responseCalls;
4598
+ formatSwitch;
4115
4599
  terminalsByStmt = /* @__PURE__ */ new Map();
4116
4600
  terminalHome = new NodeMap();
4117
4601
  attachTerminals(statement, found) {
@@ -4142,7 +4626,7 @@ var Lowerer = class {
4142
4626
  found.push(own);
4143
4627
  return found;
4144
4628
  }
4145
- for (const child of node.namedChildren) {
4629
+ for (const child of children(node)) {
4146
4630
  if (child !== null && !OWN_BODY_TYPES.has(child.type)) {
4147
4631
  this.responsesIn(child, found);
4148
4632
  }
@@ -4201,29 +4685,22 @@ var Lowerer = class {
4201
4685
  };
4202
4686
  }
4203
4687
  lowerBegin(node) {
4204
- const rescues = node.namedChildren.filter(
4205
- (child) => child !== null && child.type === "rescue"
4206
- );
4207
- const ensureClause = node.namedChildren.find(
4208
- (child) => child !== null && child.type === "ensure"
4209
- ) ?? null;
4210
- const tryBody = node.namedChildren.filter(
4688
+ const rescues2 = children(node).filter((child) => child.type === "rescue");
4689
+ const ensureClause = children(node).find((child) => child.type === "ensure") ?? null;
4690
+ const tryBody = children(node).filter(
4211
4691
  (child) => child !== null && child.type !== "rescue" && child.type !== "ensure" && child.type !== "else"
4212
4692
  );
4213
4693
  return {
4214
4694
  kind: "try",
4215
4695
  tryBody: tryBody.map((statement) => this.lower(statement)),
4216
- catchBody: rescues.length > 0 ? rescues.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4696
+ catchBody: rescues2.length > 0 ? rescues2.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4217
4697
  finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
4218
4698
  exitKind: exitKindOf(node)
4219
4699
  };
4220
4700
  }
4221
4701
  lowerCase(node) {
4222
4702
  const groups = [];
4223
- for (const child of node.namedChildren) {
4224
- if (child === null) {
4225
- continue;
4226
- }
4703
+ for (const child of children(node)) {
4227
4704
  if (child.type === "when") {
4228
4705
  groups.push({
4229
4706
  condition: handleOf(field(child, "pattern")),
@@ -4241,6 +4718,47 @@ var Lowerer = class {
4241
4718
  }
4242
4719
  return { kind: "switch", groups, exitKind: exitKindOf(node) };
4243
4720
  }
4721
+ /**
4722
+ * `respond_to do |format| ... end` runs one arm per request, picked by
4723
+ * the format the request accepts, so it lowers to a switch with an arm
4724
+ * per `format.x`. The arm that accepts every format is the default.
4725
+ * Without one, a request no arm accepts makes the library raise. Null
4726
+ * when the block contains anything besides those arms.
4727
+ */
4728
+ lowerFormatSwitch(node, block) {
4729
+ const declared = this.formatSwitch;
4730
+ if (declared === void 0 || node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== declared.name) {
4731
+ return null;
4732
+ }
4733
+ const parameter = soleBlockParameter(block);
4734
+ if (parameter === null) {
4735
+ return null;
4736
+ }
4737
+ const arms = blockBodyStatements(block).map(
4738
+ (statement) => formatArmOf(statement, parameter)
4739
+ );
4740
+ if (arms.length === 0 || arms.some((arm) => arm === null)) {
4741
+ return null;
4742
+ }
4743
+ const groups = arms.map(
4744
+ (arm) => ({
4745
+ condition: arm.format === declared.matchesEvery ? null : { sourceText: arm.header, expression: null },
4746
+ hasTrailingBreak: true,
4747
+ body: arm.block === null ? [] : this.lowerBlock(armBody(arm.block))
4748
+ })
4749
+ );
4750
+ const named2 = groups.filter((group) => group.condition !== null);
4751
+ const fallback2 = groups.find((group) => group.condition === null) ?? {
4752
+ condition: null,
4753
+ hasTrailingBreak: true,
4754
+ body: [{ kind: "exit", exit: "throw", exitKind: "throw" }]
4755
+ };
4756
+ return {
4757
+ kind: "switch",
4758
+ groups: [...named2, fallback2],
4759
+ exitKind: exitKindOf(node)
4760
+ };
4761
+ }
4244
4762
  lower(node) {
4245
4763
  const exit = exitOf(node);
4246
4764
  if (exit !== null) {
@@ -4270,10 +4788,12 @@ var Lowerer = class {
4270
4788
  [header]
4271
4789
  );
4272
4790
  }
4273
- const attachedBlock = node.namedChildren.find(
4274
- (child) => child !== null && (child.type === "do_block" || child.type === "block")
4275
- );
4276
- if (node.type === "call" && attachedBlock !== void 0) {
4791
+ const attachedBlock = attachedBlockOf(node);
4792
+ const formatSwitch = attachedBlock === null ? null : this.lowerFormatSwitch(node, attachedBlock);
4793
+ if (formatSwitch !== null) {
4794
+ return this.attach(formatSwitch, [node]);
4795
+ }
4796
+ if (node.type === "call" && attachedBlock !== null) {
4277
4797
  return this.attach(
4278
4798
  {
4279
4799
  kind: "loop",
@@ -4300,10 +4820,11 @@ var Lowerer = class {
4300
4820
  return this.attach({ kind: "opaque", exitKind: exitKindOf(node) }, [node]);
4301
4821
  }
4302
4822
  };
4303
- function lowerRubyBody(body, terminals, responseCalls = []) {
4823
+ function lowerRubyBody(body, terminals, responseCalls = [], formatSwitch) {
4304
4824
  const lowerer = new Lowerer(
4305
4825
  new Map(terminals.map((terminal) => [terminal.id, terminal])),
4306
- new NodeSet(responseCalls)
4826
+ new NodeSet(responseCalls),
4827
+ formatSwitch
4307
4828
  );
4308
4829
  return {
4309
4830
  statements: lowerer.lowerBlock(body),
@@ -4323,9 +4844,9 @@ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
4323
4844
  ]);
4324
4845
  function literalArgOf(node, facts) {
4325
4846
  const value = evaluatedValue(node, facts);
4326
- const text2 = (0, import_values7.literalOf)(value);
4327
- if (text2 !== null) {
4328
- return { kind: "string", value: text2 };
4847
+ const text3 = (0, import_values7.literalOf)(value);
4848
+ if (text3 !== null) {
4849
+ return { kind: "string", value: text3 };
4329
4850
  }
4330
4851
  const constant2 = (0, import_values7.constantOf)(value);
4331
4852
  if (typeof constant2 === "number") {
@@ -4391,7 +4912,7 @@ function isCall(node, locals, inherited) {
4391
4912
  }
4392
4913
  function collectCalls(node, read, inherited, found) {
4393
4914
  for (const child of children(node)) {
4394
- if (read.stops.has(child.type)) {
4915
+ if (read.stops.has(child.type) && !isLambdaArgument(child)) {
4395
4916
  continue;
4396
4917
  }
4397
4918
  if (isCall(child, read.locals, inherited)) {
@@ -4401,6 +4922,9 @@ function collectCalls(node, read, inherited, found) {
4401
4922
  }
4402
4923
  return found;
4403
4924
  }
4925
+ function isLambdaArgument(node) {
4926
+ return node.type === LAMBDA_TYPE && node.parent?.type === "argument_list";
4927
+ }
4404
4928
  function methodBody(definitionNode) {
4405
4929
  const body = field(definitionNode, "body");
4406
4930
  if (body === null) {
@@ -4464,6 +4988,14 @@ function enclosingStatement(call, body) {
4464
4988
  return current;
4465
4989
  }
4466
4990
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4991
+ return invocationSites(
4992
+ definitionNode,
4993
+ inherited,
4994
+ keepsArglessCall,
4995
+ facts
4996
+ ).map((site) => site.effect);
4997
+ }
4998
+ function invocationSites(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4467
4999
  const read = methodBody(definitionNode);
4468
5000
  if (read === null) {
4469
5001
  return [];
@@ -4476,7 +5008,7 @@ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, ke
4476
5008
  inherited,
4477
5009
  keepsArglessCall,
4478
5010
  facts
4479
- );
5011
+ ).map((site) => site.effect);
4480
5012
  }
4481
5013
  function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4482
5014
  const body = read.body;
@@ -4510,19 +5042,18 @@ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4510
5042
  statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
4511
5043
  );
4512
5044
  return {
4513
- type: "invocation",
4514
- callee: calleeText(call),
4515
- args: argsOf(call, facts),
4516
- async: false,
4517
- ...conditions.length > 0 ? { preconditions: conditions } : {}
5045
+ call,
5046
+ effect: {
5047
+ type: "invocation",
5048
+ callee: calleeText(call),
5049
+ args: argsOf(call, facts),
5050
+ async: false,
5051
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
5052
+ }
4518
5053
  };
4519
5054
  });
4520
5055
  }
4521
5056
 
4522
- // src/responseStatus.ts
4523
- var import_extractor4 = require("@suss/extractor");
4524
- var import_values12 = require("@suss/values");
4525
-
4526
5057
  // src/paths/predicates.ts
4527
5058
  var OPERATORS = {
4528
5059
  "==": "eq",
@@ -4541,11 +5072,11 @@ var opaqueOf = (node) => ({
4541
5072
  var JOINING = /* @__PURE__ */ new Set(["&&", "||", "and", "or"]);
4542
5073
  function predicateOf(node, inputs) {
4543
5074
  if (node.type === "parenthesized_statements") {
4544
- const inner = node.namedChildren[0];
5075
+ const inner = node.namedChild(0);
4545
5076
  return inner == null ? opaqueOf(node) : predicateOf(inner, inputs);
4546
5077
  }
4547
5078
  if (node.type === "unary" && field(node, "operator")?.text === "!") {
4548
- const operand2 = field(node, "operand") ?? node.namedChildren[0];
5079
+ const operand2 = field(node, "operand") ?? node.namedChild(0);
4549
5080
  return operand2 == null ? opaqueOf(node) : { type: "negation", operand: predicateOf(operand2, inputs) };
4550
5081
  }
4551
5082
  if (node.type === "binary") {
@@ -4571,11 +5102,11 @@ function predicateOf(node, inputs) {
4571
5102
  }
4572
5103
  function operandsOf(node) {
4573
5104
  if (node.type === "parenthesized_statements") {
4574
- const inner = node.namedChildren[0];
5105
+ const inner = node.namedChild(0);
4575
5106
  return inner == null ? [] : operandsOf(inner);
4576
5107
  }
4577
5108
  if (node.type === "unary" && field(node, "operator")?.text === "!") {
4578
- const operand2 = field(node, "operand") ?? node.namedChildren[0];
5109
+ const operand2 = field(node, "operand") ?? node.namedChild(0);
4579
5110
  return operand2 == null ? [] : operandsOf(operand2);
4580
5111
  }
4581
5112
  if (node.type === "binary") {
@@ -4671,8 +5202,317 @@ function calledMethod(node) {
4671
5202
  return field(node, "method")?.text ?? null;
4672
5203
  }
4673
5204
 
4674
- // src/provenance.ts
5205
+ // src/responseHandOff.ts
4675
5206
  var import_extractor3 = require("@suss/extractor");
5207
+
5208
+ // src/exceptionClasses.ts
5209
+ var ROOT_EXCEPTION = "Exception";
5210
+ var RUBY_EXCEPTION_PARENTS = {
5211
+ NoMemoryError: ROOT_EXCEPTION,
5212
+ ScriptError: ROOT_EXCEPTION,
5213
+ LoadError: "ScriptError",
5214
+ NotImplementedError: "ScriptError",
5215
+ SyntaxError: "ScriptError",
5216
+ SecurityError: ROOT_EXCEPTION,
5217
+ SignalException: ROOT_EXCEPTION,
5218
+ Interrupt: "SignalException",
5219
+ StandardError: ROOT_EXCEPTION,
5220
+ SystemExit: ROOT_EXCEPTION,
5221
+ SystemStackError: ROOT_EXCEPTION,
5222
+ ArgumentError: "StandardError",
5223
+ UncaughtThrowError: "ArgumentError",
5224
+ EncodingError: "StandardError",
5225
+ FiberError: "StandardError",
5226
+ IOError: "StandardError",
5227
+ EOFError: "IOError",
5228
+ IndexError: "StandardError",
5229
+ KeyError: "IndexError",
5230
+ StopIteration: "IndexError",
5231
+ ClosedQueueError: "StopIteration",
5232
+ LocalJumpError: "StandardError",
5233
+ NameError: "StandardError",
5234
+ NoMethodError: "NameError",
5235
+ RangeError: "StandardError",
5236
+ FloatDomainError: "RangeError",
5237
+ RegexpError: "StandardError",
5238
+ RuntimeError: "StandardError",
5239
+ FrozenError: "RuntimeError",
5240
+ SystemCallError: "StandardError",
5241
+ ThreadError: "StandardError",
5242
+ TypeError: "StandardError",
5243
+ ZeroDivisionError: "StandardError"
5244
+ };
5245
+ var MESSAGE_ONLY_EXCEPTION = "RuntimeError";
5246
+ var ExceptionReader = class {
5247
+ constructor(lookup, library) {
5248
+ this.library = library;
5249
+ this.walking = {
5250
+ ...lookup,
5251
+ ancestryRootClassNames: [
5252
+ ...Object.keys(library),
5253
+ ...Object.keys(RUBY_EXCEPTION_PARENTS),
5254
+ ROOT_EXCEPTION
5255
+ ]
5256
+ };
5257
+ }
5258
+ library;
5259
+ walking;
5260
+ known = /* @__PURE__ */ new Map();
5261
+ read(ref) {
5262
+ const key = [ref.text, ...ref.candidates].join("\0");
5263
+ let found = this.known.get(key);
5264
+ if (found === void 0) {
5265
+ found = readException(ref, this.walking, this.library);
5266
+ this.known.set(key, found);
5267
+ }
5268
+ return found;
5269
+ }
5270
+ };
5271
+ function listedException(name, library) {
5272
+ const fromLibrary = library[name];
5273
+ if (fromLibrary !== void 0) {
5274
+ return {
5275
+ name,
5276
+ ancestors: fromLibrary.ancestors,
5277
+ incomplete: false,
5278
+ inheritableByUnread: false
5279
+ };
5280
+ }
5281
+ const fromRuby = rubyAncestors(name);
5282
+ return fromRuby === null ? null : {
5283
+ name,
5284
+ ancestors: fromRuby,
5285
+ incomplete: false,
5286
+ inheritableByUnread: true
5287
+ };
5288
+ }
5289
+ function unreadException(name) {
5290
+ return {
5291
+ name,
5292
+ ancestors: [ROOT_EXCEPTION],
5293
+ incomplete: true,
5294
+ inheritableByUnread: true
5295
+ };
5296
+ }
5297
+ function classesOf(exception) {
5298
+ return [exception.name, ...exception.ancestors];
5299
+ }
5300
+ async function readException(ref, lookup, library) {
5301
+ for (const candidate of ref.candidates) {
5302
+ const known = listedException(candidate, library);
5303
+ if (known !== null) {
5304
+ return known;
5305
+ }
5306
+ const defined = await reachConstant([candidate], lookup);
5307
+ if (defined !== null) {
5308
+ return projectException(defined, lookup, library);
5309
+ }
5310
+ }
5311
+ return unreadException(ref.candidates.at(-1) ?? ref.text);
5312
+ }
5313
+ async function projectException(defined, lookup, library) {
5314
+ const chain = await ancestryOf(defined.name, defined.blocks, lookup);
5315
+ const ancestors = [];
5316
+ let incomplete = false;
5317
+ for (const entry of chain) {
5318
+ if (entry.type === "bodies" && entry.name === defined.name) {
5319
+ continue;
5320
+ }
5321
+ ancestors.push(entry.name);
5322
+ if (entry.type === "root") {
5323
+ ancestors.push(
5324
+ ...listedException(entry.name, library)?.ancestors ?? []
5325
+ );
5326
+ }
5327
+ if (entry.type === "unfollowed") {
5328
+ incomplete = true;
5329
+ }
5330
+ }
5331
+ if (incomplete && !ancestors.includes(ROOT_EXCEPTION)) {
5332
+ ancestors.push(ROOT_EXCEPTION);
5333
+ }
5334
+ return {
5335
+ name: defined.name,
5336
+ ancestors,
5337
+ incomplete,
5338
+ inheritableByUnread: false
5339
+ };
5340
+ }
5341
+ function rubyAncestors(name) {
5342
+ if (name === ROOT_EXCEPTION) {
5343
+ return [];
5344
+ }
5345
+ if (RUBY_EXCEPTION_PARENTS[name] === void 0) {
5346
+ return null;
5347
+ }
5348
+ const ancestors = [];
5349
+ for (let parent = RUBY_EXCEPTION_PARENTS[name]; parent !== void 0; parent = RUBY_EXCEPTION_PARENTS[parent]) {
5350
+ ancestors.push(parent);
5351
+ }
5352
+ return ancestors;
5353
+ }
5354
+
5355
+ // src/raises.ts
5356
+ var RESCUING_BODIES = /* @__PURE__ */ new Set(["begin", "body_statement"]);
5357
+ var AFTER_CATCH = /* @__PURE__ */ new Set(["rescue", "else", "ensure"]);
5358
+ function escapingRaises(method) {
5359
+ const body = field(method, "body");
5360
+ if (body === null) {
5361
+ return [];
5362
+ }
5363
+ const found = [];
5364
+ const visit = (node) => {
5365
+ for (const child of children(node)) {
5366
+ if (child === null || OWN_BODY_TYPES.has(child.type)) {
5367
+ continue;
5368
+ }
5369
+ if (isRaise(child) && !caughtLocally(child, method)) {
5370
+ found.push(child);
5371
+ continue;
5372
+ }
5373
+ visit(child);
5374
+ }
5375
+ };
5376
+ visit(body);
5377
+ return found;
5378
+ }
5379
+ function caughtLocally(raise, method) {
5380
+ let child = raise;
5381
+ for (let parent = raise.parent; parent !== null && parent.id !== method.id; parent = parent.parent) {
5382
+ if (rescues(parent, child)) {
5383
+ return true;
5384
+ }
5385
+ child = parent;
5386
+ }
5387
+ return false;
5388
+ }
5389
+ function rescues(parent, child) {
5390
+ if (parent.type === "rescue_modifier") {
5391
+ return field(parent, "body")?.id === child.id;
5392
+ }
5393
+ return RESCUING_BODIES.has(parent.type) && !AFTER_CATCH.has(child.type) && children(parent).some((one) => one?.type === "rescue");
5394
+ }
5395
+ function raisedClassRef(call, nesting) {
5396
+ const first = readCallArgs(field(call, "arguments")).positional[0];
5397
+ if (first === void 0) {
5398
+ return null;
5399
+ }
5400
+ if (first.type === "string") {
5401
+ return {
5402
+ text: MESSAGE_ONLY_EXCEPTION,
5403
+ candidates: [MESSAGE_ONLY_EXCEPTION]
5404
+ };
5405
+ }
5406
+ const constant2 = constructedClass(first) ?? first;
5407
+ const candidates = constantRefCandidates(constant2, nesting);
5408
+ return candidates.length === 0 ? null : { text: constant2.text, candidates };
5409
+ }
5410
+ function constructedClass(node) {
5411
+ if (node.type !== "call" || field(node, "method")?.text !== "new") {
5412
+ return null;
5413
+ }
5414
+ return field(node, "receiver");
5415
+ }
5416
+
5417
+ // src/responseHandOff.ts
5418
+ function raiseEndings(method) {
5419
+ return escapingRaises(method).map((call) => ({
5420
+ call,
5421
+ terminal: raiseTerminal(call)
5422
+ }));
5423
+ }
5424
+ function raiseTerminal(call) {
5425
+ const [first, second] = readCallArgs(field(call, "arguments")).positional;
5426
+ const message = first === void 0 ? null : stringLiteralValue(first);
5427
+ return {
5428
+ kind: "throw",
5429
+ statusCode: null,
5430
+ body: null,
5431
+ exceptionType: raisedClassRef(call, [])?.text ?? null,
5432
+ message: message ?? (second === void 0 ? null : stringLiteralValue(second)),
5433
+ component: null,
5434
+ renderTree: null,
5435
+ delegateTarget: null,
5436
+ emitEvent: null,
5437
+ location: rangeOf(call)
5438
+ };
5439
+ }
5440
+ function throughResponseHelpers(branches, sites, options) {
5441
+ const handOffs = [];
5442
+ for (const { call, effect } of sites) {
5443
+ const helper = helperBranchesAt(call, options);
5444
+ if (helper !== null) {
5445
+ handOffs.push({
5446
+ helper,
5447
+ preconditions: effect.preconditions,
5448
+ line: rangeOf(call).start,
5449
+ at: rangeOf(call)
5450
+ });
5451
+ }
5452
+ }
5453
+ return handOffs.length === 0 ? [...branches] : branches.flatMap((branch) => (0, import_extractor3.splitAtHandOffs)(branch, handOffs));
5454
+ }
5455
+ function helperBranchesAt(call, options) {
5456
+ if (call.type !== "call") {
5457
+ return null;
5458
+ }
5459
+ const args = readCallArgs(field(call, "arguments"));
5460
+ const handedAt = args.positional.flatMap(
5461
+ (argument, position) => argument.type === "identifier" && refersToResponse(argument, options) ? [{ position, variable: argument.text }] : []
5462
+ );
5463
+ const handedAs = Object.entries(args.keyword).flatMap(
5464
+ ([name, argument]) => argument.type === "identifier" && refersToResponse(argument, options) ? [{ name, variable: argument.text }] : []
5465
+ );
5466
+ if (handedAt.length === 0 && handedAs.length === 0) {
5467
+ return null;
5468
+ }
5469
+ const helper = calledDefinition(call, options.facts);
5470
+ if (helper === null) {
5471
+ return null;
5472
+ }
5473
+ const passed = /* @__PURE__ */ new Map();
5474
+ const names = parametersOf(helper).map(
5475
+ (parameter) => paramNameOf(parameter)?.text ?? null
5476
+ );
5477
+ for (const { position, variable } of handedAt) {
5478
+ const name = names[position];
5479
+ if (name !== void 0 && name !== null) {
5480
+ passed.set(name, variable);
5481
+ }
5482
+ }
5483
+ for (const { name, variable } of handedAs) {
5484
+ if (names.includes(name)) {
5485
+ passed.set(name, variable);
5486
+ }
5487
+ }
5488
+ return passed.size === 0 ? null : (0, import_extractor3.helperPathsOnVariables)(options.readHelper(helper), passed);
5489
+ }
5490
+ function refersToResponse(name, options) {
5491
+ const written = writtenNodeOf(name, options.facts);
5492
+ if (written === null) {
5493
+ return false;
5494
+ }
5495
+ if (sameNode(written, options.request)) {
5496
+ return true;
5497
+ }
5498
+ const member = written.type === "call" ? field(written, "method") : null;
5499
+ const receiver = written.type === "call" ? field(written, "receiver") : null;
5500
+ if (member === null || receiver?.type !== "identifier" || !options.bodyMethods.includes(member.text)) {
5501
+ return false;
5502
+ }
5503
+ const parsedFrom = writtenNodeOf(receiver, options.facts);
5504
+ return parsedFrom !== null && sameNode(parsedFrom, options.request);
5505
+ }
5506
+ function sameNode(a, b) {
5507
+ return a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.tree === b.tree;
5508
+ }
5509
+
5510
+ // src/responseStatus.ts
5511
+ var import_extractor6 = require("@suss/extractor");
5512
+ var import_values13 = require("@suss/values");
5513
+
5514
+ // src/provenance.ts
5515
+ var import_extractor4 = require("@suss/extractor");
4676
5516
  var import_values11 = require("@suss/values");
4677
5517
 
4678
5518
  // src/storage.ts
@@ -4738,7 +5578,7 @@ function pickedSource(call, loader) {
4738
5578
  return null;
4739
5579
  }
4740
5580
  const args = field(pick, "arguments");
4741
- const given = args === null ? void 0 : args.namedChildren[source.at];
5581
+ const given = args === null ? void 0 : args.namedChild(source.at);
4742
5582
  return given?.type === "constant" || given?.type === "scope_resolution" ? given : null;
4743
5583
  }
4744
5584
 
@@ -5383,10 +6223,7 @@ function recognizeMethodStorage(method, file, options) {
5383
6223
  return { effects, slots };
5384
6224
  }
5385
6225
  function callsUnder(node, found = []) {
5386
- for (const child of node.namedChildren) {
5387
- if (child === null) {
5388
- continue;
5389
- }
6226
+ for (const child of children(node)) {
5390
6227
  if (child.type === "call") {
5391
6228
  found.push(child);
5392
6229
  }
@@ -5528,7 +6365,7 @@ function valueSources(values, context, question = "wantedSource") {
5528
6365
  const spelling = spellingFor(context);
5529
6366
  const found = new NodeMap();
5530
6367
  for (const [value, key] of keys) {
5531
- found.set(value, (0, import_extractor3.sourceRefsOf)(leaves.get(key) ?? [], spelling));
6368
+ found.set(value, (0, import_extractor4.sourceRefsOf)(leaves.get(key) ?? [], spelling));
5532
6369
  }
5533
6370
  return found;
5534
6371
  }
@@ -5594,6 +6431,111 @@ function literalAt(key, facts) {
5594
6431
  return constant2 !== void 0 ? constant2 : (0, import_values11.literalOf)(value) ?? void 0;
5595
6432
  }
5596
6433
 
6434
+ // src/raisedStatuses.ts
6435
+ function rescuedBy(handlers, exception) {
6436
+ if (handlers === "eachRoute") {
6437
+ return true;
6438
+ }
6439
+ const classes = new Set(classesOf(exception));
6440
+ return handlers.some(
6441
+ (caught) => caught.someUnread || caught.classes.some(
6442
+ (one) => classes.has(one.name) || exception.incomplete && one.inheritableByUnread
6443
+ )
6444
+ );
6445
+ }
6446
+ function raisedTerminal(pattern, exception, handlers, location) {
6447
+ const libraryStatus = exception === null ? void 0 : pattern.libraryExceptions?.[exception.name]?.status;
6448
+ const status = exception === null || rescuedBy(handlers, exception) ? void 0 : libraryStatus;
6449
+ return {
6450
+ kind: status === void 0 ? "throw" : "response",
6451
+ statusCode: status === void 0 ? null : { type: "literal", value: status },
6452
+ body: null,
6453
+ exceptionType: status === void 0 ? exception?.name ?? null : null,
6454
+ ...status === void 0 && exception !== null ? {
6455
+ exceptionAncestry: {
6456
+ ancestors: exception.ancestors,
6457
+ incomplete: exception.incomplete
6458
+ }
6459
+ } : {},
6460
+ ...status === void 0 && libraryStatus !== void 0 ? { statusWhenUncaught: libraryStatus } : {},
6461
+ message: null,
6462
+ component: null,
6463
+ renderTree: null,
6464
+ delegateTarget: null,
6465
+ emitEvent: null,
6466
+ location
6467
+ };
6468
+ }
6469
+ function raisedStatusBranches(pattern, storage, effects, handlers, location) {
6470
+ const operations = new Set(
6471
+ (effects ?? []).flatMap(
6472
+ (effect) => effect.type === "interaction" && effect.interaction.class === "storage-access" && effect.interaction.operation !== void 0 ? [effect.interaction.operation] : []
6473
+ )
6474
+ );
6475
+ const raised = new Set(
6476
+ storage.flatMap(
6477
+ (one) => (one.raises ?? []).flatMap(
6478
+ (raise) => raise.methods.some((method) => operations.has(method)) ? [raise.exception] : []
6479
+ )
6480
+ )
6481
+ );
6482
+ return [...raised].flatMap((name) => {
6483
+ const exception = listedException(name, pattern.libraryExceptions ?? {}) ?? unreadException(name);
6484
+ const terminal = raisedTerminal(pattern, exception, handlers, location);
6485
+ if (terminal.kind === "throw" && !rescuedBy(handlers, exception)) {
6486
+ return [];
6487
+ }
6488
+ return [
6489
+ {
6490
+ conditions: [
6491
+ {
6492
+ sourceText: `${name} raised`,
6493
+ structured: null,
6494
+ polarity: "positive",
6495
+ source: "earlyThrow"
6496
+ }
6497
+ ],
6498
+ terminal,
6499
+ effects: [],
6500
+ location,
6501
+ isDefault: false
6502
+ }
6503
+ ];
6504
+ });
6505
+ }
6506
+
6507
+ // src/responseBody.ts
6508
+ var import_extractor5 = require("@suss/extractor");
6509
+ var import_values12 = require("@suss/values");
6510
+ var BODY_KINDS = /* @__PURE__ */ new Set([
6511
+ "record",
6512
+ "sequence",
6513
+ "unbounded"
6514
+ ]);
6515
+ function serializedValue(node, serializers) {
6516
+ const receiver = field(node, "receiver");
6517
+ const method = field(node, "method")?.text;
6518
+ if (node.type !== "call" || receiver === null || method === void 0 || !serializers.has(method) || field(node, "arguments") !== null) {
6519
+ return node;
6520
+ }
6521
+ return serializedValue(receiver, serializers);
6522
+ }
6523
+ function bodyReadingOfCall(call, declaration, facts) {
6524
+ if (declaration.bodyKeyword === void 0 || call.type !== "call") {
6525
+ return import_extractor5.absentReading;
6526
+ }
6527
+ const argument = readCallArgs(field(call, "arguments")).keyword[declaration.bodyKeyword];
6528
+ if (argument === void 0) {
6529
+ return import_extractor5.absentReading;
6530
+ }
6531
+ const body = serializedValue(
6532
+ argument,
6533
+ new Set(declaration.bodySerializers ?? [])
6534
+ );
6535
+ const value = evaluatedValue(body, facts);
6536
+ return BODY_KINDS.has(value.kind) ? (0, import_extractor5.writtenReading)((0, import_values12.shapeOfValue)(value), rangeOf(argument)) : import_extractor5.absentReading;
6537
+ }
6538
+
5597
6539
  // src/responseStatus.ts
5598
6540
  function statusArgumentOf(call, declaration) {
5599
6541
  const args = readCallArgs(field(call, "arguments"));
@@ -5606,13 +6548,17 @@ function statusArgumentOf(call, declaration) {
5606
6548
  }
5607
6549
  return args.positional[declaration.statusArgument] ?? null;
5608
6550
  }
5609
- function statusNumberOf(node, names, facts) {
6551
+ function statusNumberOf(node, names, facts, bound = /* @__PURE__ */ new Map()) {
6552
+ const passed = node.type === "identifier" ? bound.get(node.text) : void 0;
6553
+ if (passed !== void 0) {
6554
+ return passed;
6555
+ }
5610
6556
  const value = evaluatedValue(node, facts);
5611
- const constant2 = (0, import_values12.constantOf)(value);
6557
+ const constant2 = (0, import_values13.constantOf)(value);
5612
6558
  if (typeof constant2 === "number") {
5613
6559
  return constant2;
5614
6560
  }
5615
- const name = (0, import_values12.literalOf)(value);
6561
+ const name = (0, import_values13.literalOf)(value);
5616
6562
  return name === null ? null : names[name] ?? null;
5617
6563
  }
5618
6564
  function declarationsByName(declarations) {
@@ -5630,21 +6576,42 @@ function declarationOf(node, byName, locals) {
5630
6576
  const name = field(node, "method")?.text;
5631
6577
  return name === void 0 ? void 0 : byName.get(name);
5632
6578
  }
5633
- function collectResponseCalls(node, byName, locals, found) {
5634
- for (const child of node.namedChildren) {
6579
+ function responderOf(node, responders) {
6580
+ const declaration = declarationOf(node, responders.byName, responders.locals);
6581
+ if (declaration !== void 0) {
6582
+ return { kind: "declared", declaration };
6583
+ }
6584
+ const name = receiverlessName(node, responders.locals);
6585
+ const reading = name === null || responders.helper === void 0 ? null : responders.helper(name, node);
6586
+ return reading === null ? void 0 : { kind: "helper", reading };
6587
+ }
6588
+ function receiverlessName(node, locals) {
6589
+ if (node.type === "identifier") {
6590
+ return isBareMethodCall(node, locals) ? node.text : null;
6591
+ }
6592
+ if (node.type !== "call" || field(node, "receiver") !== null) {
6593
+ return null;
6594
+ }
6595
+ return field(node, "method")?.text ?? null;
6596
+ }
6597
+ function readingOfResponder(call, responder, names, options) {
6598
+ return responder.kind === "helper" ? responder.reading : readingOfCall(call, responder.declaration, names, options);
6599
+ }
6600
+ function collectResponseCalls(node, responders, found) {
6601
+ for (const child of children(node)) {
5635
6602
  if (child === null || OWN_BODY_TYPES.has(child.type)) {
5636
6603
  continue;
5637
6604
  }
5638
- if (declarationOf(child, byName, locals) !== void 0) {
6605
+ if (responderOf(child, responders) !== void 0) {
5639
6606
  found.push(child);
5640
6607
  continue;
5641
6608
  }
5642
- collectResponseCalls(child, byName, locals, found);
6609
+ collectResponseCalls(child, responders, found);
5643
6610
  }
5644
6611
  return found;
5645
6612
  }
5646
6613
  function collectReturns(node, found) {
5647
- for (const child of node.namedChildren) {
6614
+ for (const child of children(node)) {
5648
6615
  if (child === null || OWN_BODY_TYPES.has(child.type)) {
5649
6616
  continue;
5650
6617
  }
@@ -5656,23 +6623,28 @@ function collectReturns(node, found) {
5656
6623
  }
5657
6624
  return found;
5658
6625
  }
5659
- function readingOfCall(call, declaration, names, facts) {
6626
+ function readingOfCall(call, declaration, names, options) {
5660
6627
  const argument = statusArgumentOf(call, declaration);
5661
6628
  if (argument === null) {
5662
- return declaration.defaultStatusCode === void 0 ? import_extractor4.absentReading : (0, import_extractor4.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
6629
+ return declaration.defaultStatusCode === void 0 ? import_extractor6.absentReading : (0, import_extractor6.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
5663
6630
  }
5664
- const status = statusNumberOf(argument, names, facts);
6631
+ const status = statusNumberOf(
6632
+ argument,
6633
+ names,
6634
+ options.facts,
6635
+ options.boundStatuses
6636
+ );
5665
6637
  if (status === null) {
5666
- return (0, import_extractor4.unreadableReading)(
6638
+ return (0, import_extractor6.unreadableReading)(
5667
6639
  "This response writes a status that does not settle on a number here, so this outcome claims none",
5668
6640
  rangeOf(argument)
5669
6641
  );
5670
6642
  }
5671
- return (0, import_extractor4.writtenReading)(status, rangeOf(argument));
6643
+ return (0, import_extractor6.writtenReading)(status, rangeOf(argument));
5672
6644
  }
5673
6645
  function effectsReaching(effects, conditions) {
5674
6646
  return effects.filter(
5675
- (effect) => (0, import_extractor4.guardsHoldOn)(
6647
+ (effect) => (0, import_extractor6.guardsHoldOn)(
5676
6648
  effect.type === "invocation" ? effect.preconditions : void 0,
5677
6649
  conditions
5678
6650
  )
@@ -5697,6 +6669,45 @@ function conditionInputs(method, paths, facts) {
5697
6669
  );
5698
6670
  return operands.length === 0 ? void 0 : guardInputs(operands, { facts, unit: method });
5699
6671
  }
6672
+ function boundStatusArguments(helper, call, pattern, facts) {
6673
+ const parameters = children(field(helper, "parameters")).filter(
6674
+ (child) => child.type === "identifier"
6675
+ );
6676
+ const args = call.type === "call" ? readCallArgs(field(call, "arguments")).positional : [];
6677
+ const bound = /* @__PURE__ */ new Map();
6678
+ parameters.forEach((parameter, index) => {
6679
+ const arg = args[index];
6680
+ const status = arg === void 0 ? null : statusNumberOf(arg, pattern.statusCodeNames ?? {}, facts);
6681
+ if (status !== null) {
6682
+ bound.set(parameter.text, status);
6683
+ }
6684
+ });
6685
+ return bound;
6686
+ }
6687
+ function helperResponse(helper, pattern, facts, boundStatuses = /* @__PURE__ */ new Map()) {
6688
+ const branches = responseBranches(helper, pattern, [], void 0, {
6689
+ fallthrough: "handOn",
6690
+ facts,
6691
+ boundStatuses
6692
+ });
6693
+ if (branches === null || branches.some((branch) => branch.terminal.kind !== "response")) {
6694
+ return null;
6695
+ }
6696
+ const statuses = new Set(
6697
+ branches.map((branch) => {
6698
+ const reading = branch.statusCodeReading?.reading;
6699
+ if (reading?.kind === "absent") {
6700
+ return branch.statusCodeReading?.libraryDefault ?? null;
6701
+ }
6702
+ return reading?.kind === "written" ? reading.value : null;
6703
+ })
6704
+ );
6705
+ const [only] = [...statuses];
6706
+ return statuses.size === 1 && typeof only === "number" ? (0, import_extractor6.writtenReading)(only, rangeOf(helper)) : (0, import_extractor6.unreadableReading)(
6707
+ "This response is sent by a helper whose status does not settle on one number here, so this outcome claims none",
6708
+ rangeOf(helper)
6709
+ );
6710
+ }
5700
6711
  function branchOf(outcome, pattern, effects, extraEffects, options) {
5701
6712
  if (outcome.fellThrough === true && options.fallthrough === "handOn") {
5702
6713
  return handsOnBranch(outcome, effects, extraEffects);
@@ -5719,6 +6730,7 @@ function branchOf(outcome, pattern, effects, extraEffects, options) {
5719
6730
  reading: outcome.reading,
5720
6731
  libraryDefault: pattern.defaultStatusCode
5721
6732
  },
6733
+ ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
5722
6734
  effects: effectsReaching(effects, outcome.conditions),
5723
6735
  ...extraEffects === void 0 ? {} : { extraEffects },
5724
6736
  location: outcome.location,
@@ -5746,20 +6758,26 @@ function handsOnBranch(outcome, effects, extraEffects) {
5746
6758
  isDefault: outcome.conditions.length === 0
5747
6759
  };
5748
6760
  }
5749
- function returnPathBranches(method, effects, exits = [], facts) {
6761
+ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = []) {
5750
6762
  const body = field(method, "body");
5751
6763
  if (body === null) {
5752
6764
  return null;
5753
6765
  }
5754
6766
  const returns = collectReturns(body, []);
5755
6767
  const exitCalls = exits.map((exit) => exit.call);
5756
- const lowered = lowerRubyBody(body, [...returns, ...exitCalls], exitCalls);
5757
- const enumerated = (0, import_extractor4.enumerateOrDegrade)(
6768
+ const terminals = [
6769
+ ...returns,
6770
+ ...exitCalls,
6771
+ ...raises2.map((raise) => raise.call)
6772
+ ];
6773
+ const lowered = lowerRubyBody(body, terminals, exitCalls);
6774
+ const enumerated = (0, import_extractor6.enumerateOrDegrade)(
5758
6775
  {
5759
6776
  statements: lowered.statements,
5760
- terminalsByStmt: lowered.terminalsByStmt
6777
+ terminalsByStmt: lowered.terminalsByStmt,
6778
+ ...keepsArms === void 0 ? {} : { keepsArms }
5761
6779
  },
5762
- [...returns, ...exitCalls]
6780
+ terminals
5763
6781
  );
5764
6782
  const inputs = conditionInputs(
5765
6783
  method,
@@ -5787,8 +6805,8 @@ function returnPathBranches(method, effects, exits = [], facts) {
5787
6805
  returnTerminal(rangeOf(statement))
5788
6806
  );
5789
6807
  }
5790
- for (const exit of exits) {
5791
- push(enumerated.byTerminal.get(exit.call) ?? [], exit.terminal);
6808
+ for (const ending of [...exits, ...raises2]) {
6809
+ push(enumerated.byTerminal.get(ending.call) ?? [], ending.terminal);
5792
6810
  }
5793
6811
  push(enumerated.fallthrough, returnTerminal(rangeOf(method)));
5794
6812
  return branches.length === 0 ? null : branches;
@@ -5813,21 +6831,30 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5813
6831
  if (declarations.length === 0 || body === null) {
5814
6832
  return null;
5815
6833
  }
5816
- const byName = declarationsByName(declarations);
5817
- const locals = localNamesIn(method);
5818
- const responses = collectResponseCalls(body, byName, locals, []);
6834
+ const responders = {
6835
+ byName: declarationsByName(declarations),
6836
+ locals: localNamesIn(method),
6837
+ helper: options.respondingHelper
6838
+ };
6839
+ const responses = collectResponseCalls(body, responders, []);
5819
6840
  const returns = collectReturns(body, []);
5820
- const lowered = lowerRubyBody(body, returns, responses);
6841
+ const raises2 = options.raises === void 0 ? [] : escapingRaises(method);
6842
+ const lowered = lowerRubyBody(
6843
+ body,
6844
+ [...returns, ...raises2],
6845
+ responses,
6846
+ pattern.formatSwitch
6847
+ );
5821
6848
  const bareReturns = returns.filter(
5822
- (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
6849
+ (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, responders, []).length === 0
5823
6850
  );
5824
6851
  const terminals = [...responses, ...bareReturns];
5825
- const enumerated = (0, import_extractor4.enumerateOrDegrade)(
6852
+ const enumerated = (0, import_extractor6.enumerateOrDegrade)(
5826
6853
  {
5827
6854
  statements: lowered.statements,
5828
6855
  terminalsByStmt: lowered.terminalsByStmt
5829
6856
  },
5830
- terminals
6857
+ [...terminals, ...raises2]
5831
6858
  );
5832
6859
  const statusNames = pattern.statusCodeNames ?? {};
5833
6860
  const inputs = conditionInputs(
@@ -5838,30 +6865,58 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5838
6865
  const conditionsOf = (path7) => path7.map((condition) => conditionOf(condition, inputs));
5839
6866
  const outcomes = [];
5840
6867
  for (const terminal of terminals) {
5841
- const declaration = declarationOf(terminal, byName, locals);
5842
- const reading = declaration === void 0 ? import_extractor4.absentReading : readingOfCall(terminal, declaration, statusNames, options.facts);
6868
+ const responder = responderOf(terminal, responders);
6869
+ const reading = responder === void 0 ? import_extractor6.absentReading : readingOfResponder(terminal, responder, statusNames, options);
6870
+ const body2 = responder?.kind === "declared" ? bodyReadingOfCall(terminal, responder.declaration, options.facts) : void 0;
5843
6871
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
5844
6872
  outcomes.push({
5845
6873
  conditions: conditionsOf(path7),
5846
6874
  reading,
5847
- location: rangeOf(terminal)
6875
+ ...body2 === void 0 ? {} : { body: body2 },
6876
+ location: rangeOf(terminal),
6877
+ // A bare `return` wrote no response, so a filter that takes it
6878
+ // hands the request on the same as one that reaches its end.
6879
+ ...responder === void 0 ? { fellThrough: true } : {}
5848
6880
  });
5849
6881
  }
5850
6882
  }
5851
6883
  for (const path7 of enumerated.fallthrough) {
5852
6884
  outcomes.push({
5853
6885
  conditions: conditionsOf(path7),
5854
- reading: import_extractor4.absentReading,
6886
+ reading: import_extractor6.absentReading,
5855
6887
  location: rangeOf(method),
5856
6888
  fellThrough: true
5857
6889
  });
5858
6890
  }
5859
- if (outcomes.length === 0) {
6891
+ const read = options.raises;
6892
+ const raised = raises2.flatMap((call) => {
6893
+ const terminal = raisedTerminal(
6894
+ pattern,
6895
+ read?.classes.get(call) ?? null,
6896
+ read?.handlers ?? [],
6897
+ rangeOf(call)
6898
+ );
6899
+ return (enumerated.byTerminal.get(call) ?? []).map((path7) => {
6900
+ const conditions = conditionsOf(path7);
6901
+ return {
6902
+ conditions,
6903
+ terminal,
6904
+ effects: effectsReaching(effects, conditions),
6905
+ ...extraEffects === void 0 ? {} : { extraEffects },
6906
+ location: terminal.location,
6907
+ isDefault: false
6908
+ };
6909
+ });
6910
+ });
6911
+ if (outcomes.length === 0 && raised.length === 0) {
5860
6912
  return null;
5861
6913
  }
5862
- return outcomes.map(
5863
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
5864
- );
6914
+ return [
6915
+ ...outcomes.map(
6916
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
6917
+ ),
6918
+ ...raised
6919
+ ];
5865
6920
  }
5866
6921
 
5867
6922
  // src/clientCalls.ts
@@ -5874,7 +6929,16 @@ function clientCallUnits(root, pack, pattern, options) {
5874
6929
  }
5875
6930
  for (const call of callsUnder2(method)) {
5876
6931
  for (const request of requestCalls(call, pattern, options)) {
5877
- units.push(clientUnit(method, name, request, pattern, pack, options));
6932
+ units.push(
6933
+ clientUnit(
6934
+ method,
6935
+ name,
6936
+ { ...request, call },
6937
+ pattern,
6938
+ pack,
6939
+ options
6940
+ )
6941
+ );
5878
6942
  }
5879
6943
  }
5880
6944
  }
@@ -5932,10 +6996,7 @@ function requestArgument(call, called, pattern) {
5932
6996
  return pattern.requestObject?.attribute === called ? args.positional[0] : void 0;
5933
6997
  }
5934
6998
  function methodDefinitions(node, found = []) {
5935
- for (const child of node.namedChildren) {
5936
- if (child === null) {
5937
- continue;
5938
- }
6999
+ for (const child of children(node)) {
5939
7000
  if (child.type === "method" || child.type === "singleton_method") {
5940
7001
  found.push(child);
5941
7002
  }
@@ -5944,10 +7005,7 @@ function methodDefinitions(node, found = []) {
5944
7005
  return found;
5945
7006
  }
5946
7007
  function callsUnder2(node, found = []) {
5947
- for (const child of node.namedChildren) {
5948
- if (child === null) {
5949
- continue;
5950
- }
7008
+ for (const child of children(node)) {
5951
7009
  if (child.type === "call") {
5952
7010
  found.push(child);
5953
7011
  }
@@ -5964,20 +7022,23 @@ function requestCalls(call, pattern, options) {
5964
7022
  const read = (site) => requestCall(
5965
7023
  call,
5966
7024
  called,
5967
- receiverPrefix(receiver, pattern, options, site),
7025
+ receiverBase(receiver, pattern, options, site),
5968
7026
  pattern,
5969
7027
  options,
5970
7028
  site
5971
7029
  );
5972
7030
  const plain = read();
5973
- if (plain !== null && !(0, import_behavioral_ir.hasNameHole)(plain.path)) {
7031
+ if (plain !== null && !(0, import_behavioral_ir2.hasNameHole)(plain.path)) {
5974
7032
  return [plain];
5975
7033
  }
5976
7034
  const byBoundary = /* @__PURE__ */ new Map();
5977
7035
  for (const site of constructionSitesOf(call, options.facts)) {
5978
7036
  const stated = read(site);
5979
7037
  if (stated !== null) {
5980
- byBoundary.set(`${stated.method} ${stated.path}`, stated);
7038
+ byBoundary.set(
7039
+ `${stated.method} ${stated.host ?? ""} ${stated.path}`,
7040
+ stated
7041
+ );
5981
7042
  }
5982
7043
  }
5983
7044
  if (byBoundary.size > 0) {
@@ -5985,12 +7046,12 @@ function requestCalls(call, pattern, options) {
5985
7046
  }
5986
7047
  return plain === null ? [] : [plain];
5987
7048
  }
5988
- function requestCall(call, called, prefix, pattern, options, site) {
7049
+ function requestCall(call, called, base, pattern, options, site) {
5989
7050
  const args = readCallArgs(field(call, "arguments"));
5990
7051
  const verb = pattern.verbMethodNames[called];
5991
7052
  if (verb !== void 0) {
5992
- const path8 = urlIn(args, pattern, options, site);
5993
- return path8 === null ? null : { method: verb, path: prefix + path8 };
7053
+ const target2 = targetAt(urlNodeIn(args, pattern), options, site);
7054
+ return target2 === null ? null : { method: verb, ...underBase(base, target2) };
5994
7055
  }
5995
7056
  const sent = pattern.requestObject;
5996
7057
  if (sent === void 0 || sent.attribute !== called) {
@@ -6000,8 +7061,14 @@ function requestCall(call, called, prefix, pattern, options, site) {
6000
7061
  if (built === null) {
6001
7062
  return null;
6002
7063
  }
6003
- const path7 = pathAt(built.url, options, site);
6004
- return path7 === null ? null : { method: built.method, path: prefix + path7 };
7064
+ const target = targetAt(built.url, options, site);
7065
+ return target === null ? null : { method: built.method, ...underBase(base, target) };
7066
+ }
7067
+ function underBase(base, target) {
7068
+ if (base === null || target.host !== void 0) {
7069
+ return target;
7070
+ }
7071
+ return { path: base.path + target.path, host: base.host };
6005
7072
  }
6006
7073
  function requestBuilt(argument, sent, options, site) {
6007
7074
  if (argument === void 0) {
@@ -6022,16 +7089,19 @@ function requestBuilt(argument, sent, options, site) {
6022
7089
  function isLibraryReceiver(receiver, pattern, options) {
6023
7090
  return namesConstant2(receiver, pattern.constantName) || builderCallBehind(receiver, pattern, options.facts) !== null;
6024
7091
  }
6025
- function receiverPrefix(receiver, pattern, options, site) {
7092
+ function receiverBase(receiver, pattern, options, site) {
6026
7093
  const built = builderCallBehind(receiver, pattern, options.facts);
6027
7094
  if (built === null) {
6028
- return "";
7095
+ return null;
6029
7096
  }
6030
7097
  const args = readCallArgs(field(built, "arguments"));
6031
7098
  const keyword = pattern.builderUrlKeyword;
6032
7099
  const base = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[0];
6033
- const path7 = base === void 0 ? null : (0, import_values13.pathOf)(evaluatedValue(base, options.facts, void 0, site));
6034
- return path7 === void 0 || path7 === null ? "" : trimmed(path7);
7100
+ if (base === void 0) {
7101
+ return null;
7102
+ }
7103
+ const value = evaluatedValue(base, options.facts, void 0, site);
7104
+ return { path: trimmed((0, import_values14.pathOf)(value) ?? ""), host: (0, import_values14.hostOf)(value) };
6035
7105
  }
6036
7106
  function trimmed(path7) {
6037
7107
  return path7 === "/" ? "" : path7.replace(/\/+$/, "");
@@ -6054,22 +7124,32 @@ function builderCallBehind(receiver, pattern, facts) {
6054
7124
  const constant2 = field(written, "receiver");
6055
7125
  return constant2 !== null && namesConstant2(constant2, pattern.constantName) ? written : null;
6056
7126
  }
6057
- function urlIn(args, pattern, options, site) {
6058
- return pathAt(urlNodeIn(args, pattern), options, site);
6059
- }
6060
7127
  function urlNodeIn(args, pattern) {
6061
7128
  const keyword = pattern.url.keyword;
6062
7129
  return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
6063
7130
  }
6064
- function pathAt(written, options, site) {
7131
+ function targetAt(written, options, site) {
6065
7132
  if (written === void 0) {
6066
7133
  return null;
6067
7134
  }
6068
- const path7 = (0, import_values13.pathOf)(evaluatedValue(written, options.facts, void 0, site));
6069
- return path7 === void 0 || (0, import_behavioral_ir.namesNothing)(path7) ? null : path7;
7135
+ const value = evaluatedValue(written, options.facts, void 0, site);
7136
+ const path7 = (0, import_values14.pathOf)(value);
7137
+ return path7 === void 0 || (0, import_behavioral_ir2.namesNothing)(path7) ? null : { path: path7, host: (0, import_values14.hostOf)(value) };
6070
7138
  }
6071
7139
  function clientUnit(method, name, request, pattern, pack, options) {
6072
7140
  const range = rangeOf(method);
7141
+ const accessors = responseAccessors(pattern);
7142
+ const statusMembers = (0, import_behavioral_ir2.statusMembersOf)(accessors);
7143
+ const reading = {
7144
+ facts: options.facts,
7145
+ statusMembers,
7146
+ handOff: {
7147
+ request: request.call,
7148
+ bodyMethods: accessors.bodyAccessors ?? [],
7149
+ facts: options.facts,
7150
+ readHelper: (helper) => callerBranches(helper, { facts: options.facts, statusMembers })
7151
+ }
7152
+ };
6073
7153
  return {
6074
7154
  identity: {
6075
7155
  name,
@@ -6081,25 +7161,34 @@ function clientUnit(method, name, request, pattern, pack, options) {
6081
7161
  exportName: name,
6082
7162
  exportPath: [name]
6083
7163
  },
6084
- boundaryBinding: (0, import_behavioral_ir.restBinding)({
7164
+ boundaryBinding: (0, import_behavioral_ir2.restBinding)({
6085
7165
  transport: pack.protocol,
6086
7166
  method: request.method,
6087
7167
  path: request.path,
7168
+ host: request.host,
6088
7169
  recognition: pack.name
6089
7170
  }),
6090
7171
  parameters: [],
6091
- branches: callerBranches(method, range, options.facts),
6092
- ...responseAccessors(pattern),
7172
+ branches: callerBranches(method, reading),
7173
+ ...accessors,
6093
7174
  bodyContent: "statements",
6094
7175
  dependencyCalls: [],
6095
7176
  declaredContract: null
6096
7177
  };
6097
7178
  }
6098
- function callerBranches(method, range, facts) {
6099
- const effects = invocationEffects(method, void 0, void 0, facts);
6100
- return returnPathBranches(method, effects, [], facts) ?? [
6101
- returnBranch(range, effects)
6102
- ];
7179
+ function callerBranches(method, reading) {
7180
+ const sites = invocationSites(method, void 0, void 0, reading.facts);
7181
+ const effects = sites.map((site) => site.effect);
7182
+ const keepsArms = (condition) => (0, import_behavioral_ir2.testsStatus)(predicateOf(condition), reading.statusMembers);
7183
+ const found = returnPathBranches(
7184
+ method,
7185
+ effects,
7186
+ [],
7187
+ reading.facts,
7188
+ keepsArms,
7189
+ raiseEndings(method)
7190
+ ) ?? [returnBranch(rangeOf(method), effects)];
7191
+ return reading.handOff === void 0 ? found : throughResponseHelpers(found, sites, reading.handOff);
6103
7192
  }
6104
7193
  function responseAccessors(pattern) {
6105
7194
  const response = pattern.response;
@@ -6110,7 +7199,8 @@ function responseAccessors(pattern) {
6110
7199
  ...response.body === void 0 ? {} : { bodyAccessors: response.body },
6111
7200
  ...response.statusCode === void 0 ? {} : { statusAccessors: response.statusCode },
6112
7201
  ...response.success === void 0 ? {} : { successAccessors: response.success },
6113
- ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery }
7202
+ ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery },
7203
+ ...response.redirectDelivery === void 0 ? {} : { redirectDelivery: response.redirectDelivery }
6114
7204
  };
6115
7205
  }
6116
7206
  function returnBranch(range, effects) {
@@ -6135,8 +7225,8 @@ function returnBranch(range, effects) {
6135
7225
  }
6136
7226
 
6137
7227
  // src/envReads.ts
6138
- var import_behavioral_ir2 = require("@suss/behavioral-ir");
6139
- var import_extractor5 = require("@suss/extractor");
7228
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
7229
+ var import_extractor7 = require("@suss/extractor");
6140
7230
  var import_resolution9 = require("@suss/resolution");
6141
7231
 
6142
7232
  // src/envSpellings.ts
@@ -6161,9 +7251,7 @@ function elementSite(node) {
6161
7251
  if (object === null || !isEnv(object) || isAssignedTo(node)) {
6162
7252
  return null;
6163
7253
  }
6164
- const index = node.namedChildren.find(
6165
- (child) => child !== null && child.id !== object.id
6166
- );
7254
+ const index = children(node).find((child) => child.id !== object.id);
6167
7255
  if (index === void 0) {
6168
7256
  return null;
6169
7257
  }
@@ -6344,7 +7432,7 @@ function followsExitWhenAbsent(parent, child, subject) {
6344
7432
  if (!STATEMENT_LIST_TYPES.has(parent.type)) {
6345
7433
  return false;
6346
7434
  }
6347
- const statements = parent.namedChildren;
7435
+ const statements = children(parent);
6348
7436
  const at = statements.findIndex((statement) => statement?.id === child.id);
6349
7437
  return statements.slice(0, at).some(
6350
7438
  (statement) => statement !== null && leavesWhenAbsent(statement, subject)
@@ -6396,7 +7484,7 @@ function continuationRaises(from) {
6396
7484
  let parent = current.parent;
6397
7485
  while (parent !== null) {
6398
7486
  if (STATEMENT_LIST_TYPES.has(parent.type)) {
6399
- const statements = parent.namedChildren;
7487
+ const statements = children(parent);
6400
7488
  const at = statements.findIndex((one) => one?.id === current.id);
6401
7489
  const exit = firstExit(statements.slice(at + 1));
6402
7490
  if (exit !== null) {
@@ -6430,20 +7518,13 @@ function firstExit(statements) {
6430
7518
  return null;
6431
7519
  }
6432
7520
  function exitOf2(statement) {
6433
- if (isRaise3(statement)) {
7521
+ if (isRaiseCall(statement)) {
6434
7522
  return "raise";
6435
7523
  }
6436
7524
  if (LEAVING_TYPES.has(statement.type)) {
6437
7525
  return "leave";
6438
7526
  }
6439
- return STATEMENT_LIST_TYPES.has(statement.type) ? firstExit(statement.namedChildren) : null;
6440
- }
6441
- var RAISING_METHODS2 = /* @__PURE__ */ new Set(["raise", "fail"]);
6442
- function isRaise3(node) {
6443
- if (node.type === "identifier") {
6444
- return RAISING_METHODS2.has(node.text);
6445
- }
6446
- return node.type === "call" && field(node, "receiver") === null && RAISING_METHODS2.has(field(node, "method")?.text ?? "");
7527
+ return STATEMENT_LIST_TYPES.has(statement.type) ? firstExit(children(statement)) : null;
6447
7528
  }
6448
7529
  function isPresentWhen(test, outcome, subject) {
6449
7530
  const expression = peelParens(test);
@@ -6511,7 +7592,7 @@ function isNilCheckOn(call, receiver) {
6511
7592
  return call.type === "call" && field(call, "method")?.text === "nil?" && field(call, "receiver")?.id === receiver.id;
6512
7593
  }
6513
7594
  function peelParens(node) {
6514
- const inner = node.type === "parenthesized_statements" && node.namedChildCount === 1 ? node.namedChildren[0] : null;
7595
+ const inner = node.type === "parenthesized_statements" && node.namedChildCount === 1 ? node.namedChild(0) : null;
6515
7596
  return inner === null || inner === void 0 ? node : peelParens(inner);
6516
7597
  }
6517
7598
  function climbParens(node) {
@@ -6556,7 +7637,7 @@ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method",
6556
7637
  function envReadEffects(root, facts) {
6557
7638
  const helpers = helperFactsOf(facts);
6558
7639
  const slots = [];
6559
- (0, import_extractor5.walkDescendants)(root, null, {
7640
+ (0, import_extractor7.walkDescendants)(root, null, children, {
6560
7641
  at: (node) => {
6561
7642
  const read = envReadAt(node);
6562
7643
  if (read !== null) {
@@ -6567,7 +7648,7 @@ function envReadEffects(root, facts) {
6567
7648
  slots.push({ call: node });
6568
7649
  }
6569
7650
  },
6570
- into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? import_extractor5.SKIP_CHILDREN : null
7651
+ into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? import_extractor7.SKIP_CHILDREN : null
6571
7652
  });
6572
7653
  const calls = slots.flatMap((slot) => "call" in slot ? [slot.call] : []);
6573
7654
  const throughHelpers = helpers === null ? new NodeMap() : helperReads(calls, helpers);
@@ -6636,7 +7717,7 @@ function namedParameters(db) {
6636
7717
  return named2;
6637
7718
  }
6638
7719
  function emitEnvFacts(db, file, root) {
6639
- (0, import_extractor5.walkDescendants)(root, null, {
7720
+ (0, import_extractor7.walkDescendants)(root, null, children, {
6640
7721
  at: (node) => {
6641
7722
  if (isEnv(node) && handsOnward(node)) {
6642
7723
  db.add("environmentObject", [
@@ -6734,7 +7815,7 @@ function readsAtCall(db, call, args) {
6734
7815
  function configReadEffect(read) {
6735
7816
  return {
6736
7817
  type: "interaction",
6737
- binding: (0, import_behavioral_ir2.runtimeConfigBinding)({ recognition: RUBY_ENV_RECOGNITION }),
7818
+ binding: (0, import_behavioral_ir3.runtimeConfigBinding)({ recognition: RUBY_ENV_RECOGNITION }),
6738
7819
  callee: `ENV["${read.name}"]`,
6739
7820
  interaction: {
6740
7821
  class: "config-read",
@@ -6764,7 +7845,8 @@ function envSiteAt(node) {
6764
7845
  }
6765
7846
 
6766
7847
  // src/filters.ts
6767
- function controllerFilters(pattern, ancestry, read = {}) {
7848
+ var RESCUES_NOTHING = { refs: [], someUnread: false };
7849
+ function controllerFilterChain(pattern, ancestry, read = {}) {
6768
7850
  const forms = pattern.filters ?? [];
6769
7851
  if (forms.length === 0) {
6770
7852
  return [];
@@ -6777,18 +7859,29 @@ function controllerFilters(pattern, ancestry, read = {}) {
6777
7859
  }
6778
7860
  }
6779
7861
  let declared = [];
6780
- for (const { statement } of inheritedStatements(ancestry)) {
7862
+ const inlineSeen = /* @__PURE__ */ new Map();
7863
+ for (const { block, statement } of inheritedStatements(ancestry)) {
6781
7864
  const called = calledName(statement);
6782
7865
  if (called === null) {
6783
7866
  continue;
6784
7867
  }
6785
7868
  const form = byName.get(called);
6786
7869
  if (form !== void 0) {
6787
- for (const declaration of declarationsOf(statement, form, read.facts)) {
6788
- declared = [
6789
- ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
6790
- declaration
6791
- ];
7870
+ const inlineKey = `${block.file}\0${block.info.qualifiedName}\0${form.name}`;
7871
+ const ordinal = inlineSeen.get(inlineKey) ?? 0;
7872
+ const declarations = declarationsOf(
7873
+ statement,
7874
+ form,
7875
+ block,
7876
+ read.facts,
7877
+ ordinal
7878
+ );
7879
+ if (declarations.some((one) => one.inline !== void 0)) {
7880
+ inlineSeen.set(inlineKey, ordinal + 1);
7881
+ }
7882
+ for (const declaration of declarations) {
7883
+ const earlier = declaration.filter.onThrow === true ? void 0 : declared.find((one) => sameFilter(one, declaration));
7884
+ declared = [...declared.filter((one) => one !== earlier), declaration];
6792
7885
  }
6793
7886
  continue;
6794
7887
  }
@@ -6797,26 +7890,78 @@ function controllerFilters(pattern, ancestry, read = {}) {
6797
7890
  declared = applySkips(declared, skipsOf(statement, skipped, read.facts));
6798
7891
  }
6799
7892
  }
6800
- const resolved = [];
6801
- for (const declaration of declared) {
6802
- const found = methodInAncestry(ancestry, declaration.methodName, read);
6803
- if (found.type !== "found") {
6804
- continue;
6805
- }
6806
- resolved.push({
7893
+ const resolved = declared.map(
7894
+ (declaration) => linkFor(
7895
+ declaration,
7896
+ writtenAt(
7897
+ declaration,
7898
+ filterMethod(ancestry, declaration.methodName, read)
7899
+ )
7900
+ )
7901
+ );
7902
+ return [
7903
+ ...resolved.filter((one) => !runsAfterARaise(one)),
7904
+ ...resolved.filter(runsAfterARaise).reverse()
7905
+ ];
7906
+ }
7907
+ function linkFor(declaration, written) {
7908
+ if (written === null) {
7909
+ return {
7910
+ unread: {
7911
+ filter: declaration.filter,
7912
+ methodName: declaration.methodName,
7913
+ declaredIn: declaration.declaredIn,
7914
+ only: declaration.only,
7915
+ except: declaration.except
7916
+ }
7917
+ };
7918
+ }
7919
+ return {
7920
+ read: {
6807
7921
  filter: declaration.filter,
7922
+ ...written,
7923
+ only: declaration.only,
7924
+ except: declaration.except,
7925
+ rescues: declaration.rescues
7926
+ }
7927
+ };
7928
+ }
7929
+ function filterOf(link) {
7930
+ return "read" in link ? link.read : link.unread;
7931
+ }
7932
+ function runsAfterARaise(link) {
7933
+ return filterOf(link).filter.onThrow === true;
7934
+ }
7935
+ function writtenAt(declaration, found) {
7936
+ if (found.type === "found") {
7937
+ return {
6808
7938
  methodName: declaration.methodName,
7939
+ nameKind: "binding",
6809
7940
  method: found.method,
6810
7941
  file: found.block.file,
6811
7942
  enclosingQualifiedName: found.block.info.qualifiedName,
6812
- only: declaration.only,
6813
- except: declaration.except
6814
- });
7943
+ nesting: found.block.info.bodyNesting
7944
+ };
6815
7945
  }
6816
- return [
6817
- ...resolved.filter((one) => one.filter.onThrow !== true),
6818
- ...resolved.filter((one) => one.filter.onThrow === true)
6819
- ];
7946
+ const inline = declaration.inline;
7947
+ if (inline === void 0) {
7948
+ return null;
7949
+ }
7950
+ return {
7951
+ methodName: inline.label,
7952
+ nameKind: "label",
7953
+ method: inline.block,
7954
+ file: inline.declaredIn.file,
7955
+ enclosingQualifiedName: inline.declaredIn.info.qualifiedName,
7956
+ nesting: inline.declaredIn.info.bodyNesting
7957
+ };
7958
+ }
7959
+ function filterMethod(ancestry, name, read) {
7960
+ const found = methodInAncestry(ancestry, name, read);
7961
+ if (found.type === "unsettled" && found.cause === "unreadAncestor") {
7962
+ return methodPastUnreadAncestors(ancestry, name, read);
7963
+ }
7964
+ return found;
6820
7965
  }
6821
7966
  function filterCoversAction(filter, actionName) {
6822
7967
  if (filter.except.has(actionName)) {
@@ -6824,37 +7969,55 @@ function filterCoversAction(filter, actionName) {
6824
7969
  }
6825
7970
  return filter.only === null || filter.only.has(actionName);
6826
7971
  }
6827
- function filterReference(filter, displayPath) {
7972
+ function filterReference(filter, displayPath, caught) {
7973
+ return {
7974
+ file: displayPath,
7975
+ name: filter.methodName,
7976
+ ...filter.filter.onThrow === true ? { onThrow: true } : {},
7977
+ ...caught === void 0 ? {} : {
7978
+ catches: caught.classes.map((one) => one.name),
7979
+ ...caught.classes.some((one) => one.inheritableByUnread) ? { mayCatchUnreadClasses: true } : {},
7980
+ ...caught.someUnread ? { mayCatchAny: true } : {}
7981
+ }
7982
+ };
7983
+ }
7984
+ function unreadFilterReference(filter, displayPath) {
6828
7985
  return {
6829
7986
  file: displayPath,
6830
7987
  name: filter.methodName,
6831
7988
  ...filter.filter.onThrow === true ? { onThrow: true } : {}
6832
7989
  };
6833
7990
  }
6834
- function filterUnit(filter, pattern, displayPath, body, facts) {
7991
+ function filterUnit(filter, pattern, displayPath, body, reading = {}) {
6835
7992
  const range = rangeOf(filter.method);
7993
+ const { respondingHelper, raises: raises2 } = reading;
6836
7994
  const branches = responseBranches(
6837
7995
  filter.method,
6838
7996
  pattern,
6839
7997
  body.effects ?? [],
6840
7998
  body.extraEffects,
6841
- { fallthrough: "handOn", facts }
7999
+ {
8000
+ fallthrough: "handOn",
8001
+ facts: reading.facts,
8002
+ ...respondingHelper === void 0 ? {} : { respondingHelper },
8003
+ ...raises2 === void 0 ? {} : { raises: raises2 }
8004
+ }
6842
8005
  );
6843
8006
  return {
6844
8007
  identity: {
6845
8008
  name: filter.methodName,
6846
- nameKind: "binding",
8009
+ nameKind: filter.nameKind,
6847
8010
  kind: "middleware",
6848
8011
  file: displayPath,
6849
8012
  range,
6850
8013
  span: spanOf(filter.method),
6851
- exportName: filter.methodName,
8014
+ exportName: filter.nameKind === "binding" ? filter.methodName : null,
6852
8015
  exportPath: [filter.enclosingQualifiedName, filter.methodName]
6853
8016
  },
6854
8017
  boundaryBinding: null,
6855
8018
  parameters: [],
6856
8019
  branches: withSlotSources(
6857
- branches ?? [handsOn(range, body)],
8020
+ [...branches ?? [handsOn(range, body)], ...reading.raised ?? []],
6858
8021
  body.provenance
6859
8022
  ),
6860
8023
  bodyContent: body.bodyContent ?? "absent",
@@ -6889,25 +8052,93 @@ function calledName(statement) {
6889
8052
  }
6890
8053
  return field(statement, "method")?.text ?? null;
6891
8054
  }
6892
- function declarationsOf(statement, filter, facts) {
8055
+ function declarationsOf(statement, filter, declaredIn, facts, inlineOrdinal) {
6893
8056
  const args = readCallArgs(field(statement, "arguments"));
6894
8057
  const keywords = filter.actionKeywords;
6895
8058
  const only = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.include], facts);
6896
8059
  const except = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.exclude], facts);
6897
- return methodNamesOf(args, filter, facts).map((methodName) => ({
8060
+ const rescues2 = filter.onThrow === true ? rescuedClasses(args, declaredIn.info.bodyNesting, facts) : RESCUES_NOTHING;
8061
+ const inline = inlineHandler(
8062
+ statement,
8063
+ args,
8064
+ filter,
8065
+ declaredIn,
8066
+ inlineOrdinal
8067
+ );
8068
+ return methodNamesOf(args, filter, facts, inline).map((methodName) => ({
6898
8069
  filter,
6899
8070
  methodName,
6900
8071
  only,
6901
- except: except ?? /* @__PURE__ */ new Set()
8072
+ except: except ?? /* @__PURE__ */ new Set(),
8073
+ rescues: rescues2,
8074
+ declaredIn: declaredIn.file,
8075
+ ...inline === void 0 ? {} : { inline }
6902
8076
  }));
6903
8077
  }
6904
- function methodNamesOf(args, filter, facts) {
8078
+ function inlineHandler(statement, args, filter, declaredIn, ordinal) {
8079
+ const block = blockInPlaceOfMethod(statement, args, filter);
8080
+ if (block === null || blockStatements2(block).length === 0) {
8081
+ return void 0;
8082
+ }
8083
+ return { block, label: `${filter.name}#${ordinal}`, declaredIn };
8084
+ }
8085
+ function rescuedClasses(args, nesting, facts) {
8086
+ const refs = [];
8087
+ let someUnread = args.positional.length === 0;
8088
+ for (const arg of args.positional) {
8089
+ const candidates = constantRefCandidates(arg, nesting);
8090
+ const named2 = candidates.length === 0 ? stringValueOf(arg, facts) : null;
8091
+ if (candidates.length === 0 && named2 === null) {
8092
+ someUnread = true;
8093
+ continue;
8094
+ }
8095
+ refs.push({
8096
+ text: arg.text,
8097
+ candidates: named2 === null ? candidates : [named2]
8098
+ });
8099
+ }
8100
+ return { refs, someUnread };
8101
+ }
8102
+ function methodNamesOf(args, filter, facts, inline) {
8103
+ const named2 = filter.methodFrom === "withKeyword" ? withKeywordName(args, facts) : symbolArgumentNames(args, facts);
8104
+ if (named2.length > 0 || inline === void 0) {
8105
+ return named2;
8106
+ }
8107
+ return [blockHandlerName(inline.block) ?? inline.label];
8108
+ }
8109
+ function withKeywordName(args, facts) {
8110
+ const named2 = args.keyword.with;
8111
+ const value = named2 === void 0 ? null : stringValueOf(named2, facts);
8112
+ return value === null ? [] : [value];
8113
+ }
8114
+ function blockInPlaceOfMethod(statement, args, filter) {
6905
8115
  if (filter.methodFrom === "withKeyword") {
6906
- const named2 = args.keyword.with;
6907
- const value = named2 === void 0 ? null : stringValueOf(named2, facts);
6908
- return value === null ? [] : [value];
8116
+ return args.keyword.with === void 0 ? field(statement, "block") : null;
8117
+ }
8118
+ const lambda = args.positional.find((arg) => arg.type === "lambda");
8119
+ if (lambda !== void 0) {
8120
+ return field(lambda, "body");
8121
+ }
8122
+ return args.positional.length === 0 ? field(statement, "block") : null;
8123
+ }
8124
+ function blockStatements2(block) {
8125
+ return children(field(block, "body")).filter(
8126
+ (child) => child.type !== "comment"
8127
+ );
8128
+ }
8129
+ function blockHandlerName(block) {
8130
+ const statements = blockStatements2(block);
8131
+ const only = statements.length === 1 ? statements[0] : void 0;
8132
+ if (only === void 0) {
8133
+ return null;
8134
+ }
8135
+ const parameters = new Set(
8136
+ children(field(block, "parameters")).map((parameter) => parameter.text)
8137
+ );
8138
+ if (only.type === "identifier") {
8139
+ return parameters.has(only.text) ? null : only.text;
6909
8140
  }
6910
- return symbolArgumentNames(args, facts);
8141
+ return calledName(only);
6911
8142
  }
6912
8143
  function symbolArgumentNames(args, facts) {
6913
8144
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
@@ -6918,13 +8149,16 @@ function skipsOf(statement, filterName, facts) {
6918
8149
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
6919
8150
  }
6920
8151
  function sameFilter(one, other) {
6921
- return one.filter.name === other.filter.name && one.methodName === other.methodName;
8152
+ return namedBySymbol(one) && namedBySymbol(other) && one.filter.name === other.filter.name && one.methodName === other.methodName;
8153
+ }
8154
+ function namedBySymbol(declaration) {
8155
+ return declaration.inline === void 0;
6922
8156
  }
6923
8157
  function applySkips(declared, skips) {
6924
8158
  const left = [];
6925
8159
  for (const declaration of declared) {
6926
8160
  const reaching = skips.filter(
6927
- (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
8161
+ (skip) => namedBySymbol(declaration) && skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
6928
8162
  );
6929
8163
  if (reaching.some((skip) => skip.actions === null)) {
6930
8164
  continue;
@@ -6948,8 +8182,8 @@ function actionsUnder(node, facts) {
6948
8182
  }
6949
8183
 
6950
8184
  // src/streamWrites.ts
6951
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
6952
- var import_extractor6 = require("@suss/extractor");
8185
+ var import_behavioral_ir4 = require("@suss/behavioral-ir");
8186
+ var import_extractor8 = require("@suss/extractor");
6953
8187
  var RUBY_STREAM_RECOGNITION = "ruby-stdlib";
6954
8188
  var KERNEL_WRITES = /* @__PURE__ */ new Map([
6955
8189
  ["puts", "stdout"],
@@ -6967,14 +8201,14 @@ var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
6967
8201
  var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
6968
8202
  function streamWriteEffects(root) {
6969
8203
  const found = [];
6970
- (0, import_extractor6.walkDescendants)(root, null, {
8204
+ (0, import_extractor8.walkDescendants)(root, null, children, {
6971
8205
  at: (node) => {
6972
8206
  const write = node.type === "call" ? streamWriteAt(node) : null;
6973
8207
  if (write !== null) {
6974
8208
  found.push(write);
6975
8209
  }
6976
8210
  },
6977
- into: (node) => DEFERRED_BODY_TYPES2.has(node.type) ? import_extractor6.SKIP_CHILDREN : null
8211
+ into: (node) => DEFERRED_BODY_TYPES2.has(node.type) ? import_extractor8.SKIP_CHILDREN : null
6978
8212
  });
6979
8213
  return found;
6980
8214
  }
@@ -7005,7 +8239,7 @@ function writeEffect(call, target, args) {
7005
8239
  const method = field(call, "method")?.text ?? "";
7006
8240
  return {
7007
8241
  type: "interaction",
7008
- binding: (0, import_behavioral_ir3.ioBinding)({ recognition: RUBY_STREAM_RECOGNITION, target }),
8242
+ binding: (0, import_behavioral_ir4.ioBinding)({ recognition: RUBY_STREAM_RECOGNITION, target }),
7009
8243
  callee: receiver === null ? method : `${receiver.text}.${method}`,
7010
8244
  groupId: `${call.startPosition.row + 1}:${call.startPosition.column + 1}`,
7011
8245
  interaction: { class: "stream-write", ...payloadOf(args) }
@@ -7058,7 +8292,7 @@ function writtenShapeOf(node) {
7058
8292
  return hashShape(node);
7059
8293
  }
7060
8294
  if (node.type === "array") {
7061
- const first = node.namedChildren.find((child) => child !== null);
8295
+ const first = children(node).find((child) => child !== null);
7062
8296
  return {
7063
8297
  type: "array",
7064
8298
  items: first ? writtenShapeOf(first) : { type: "unknown" }
@@ -7068,7 +8302,7 @@ function writtenShapeOf(node) {
7068
8302
  }
7069
8303
  function hashShape(node) {
7070
8304
  const properties = {};
7071
- for (const pair of node.namedChildren) {
8305
+ for (const pair of children(node)) {
7072
8306
  if (pair?.type !== "pair") {
7073
8307
  continue;
7074
8308
  }
@@ -7086,10 +8320,10 @@ function keyName(key) {
7086
8320
  }
7087
8321
 
7088
8322
  // src/testCases.ts
7089
- var import_behavioral_ir4 = require("@suss/behavioral-ir");
7090
- var import_extractor7 = require("@suss/extractor");
8323
+ var import_behavioral_ir5 = require("@suss/behavioral-ir");
8324
+ var import_extractor9 = require("@suss/extractor");
7091
8325
  var import_resolution10 = require("@suss/resolution");
7092
- var import_values16 = require("@suss/values");
8326
+ var import_values17 = require("@suss/values");
7093
8327
  var BLOCK_TYPES6 = /* @__PURE__ */ new Set(["block", "do_block"]);
7094
8328
  function testPatternsIn(packs) {
7095
8329
  return packs.flatMap((pack) => pack.tests ?? []);
@@ -7273,7 +8507,7 @@ function groupStatements(group) {
7273
8507
  }
7274
8508
  function patternForFile(file, patterns) {
7275
8509
  return patterns.find(
7276
- (pattern) => (0, import_extractor7.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor7.isListedTestFile)(file, pattern.files)
8510
+ (pattern) => (0, import_extractor9.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor9.isListedTestFile)(file, pattern.files)
7277
8511
  ) ?? null;
7278
8512
  }
7279
8513
  function groupNamesIn(pattern) {
@@ -7431,14 +8665,14 @@ var NodesSeen = class {
7431
8665
  };
7432
8666
  function exampleRun(example, pattern, shared) {
7433
8667
  const byName = /* @__PURE__ */ new Map();
7434
- const always2 = [];
8668
+ const always3 = [];
7435
8669
  for (const group of withInlineIncludes(example.groups, pattern, shared)) {
7436
8670
  for (const one of groupBlocks(group, pattern)) {
7437
8671
  for (const name of one.names) {
7438
8672
  byName.set(name, one.block);
7439
8673
  }
7440
8674
  if (one.always) {
7441
- always2.push(one.block);
8675
+ always3.push(one.block);
7442
8676
  }
7443
8677
  }
7444
8678
  }
@@ -7457,7 +8691,7 @@ function exampleRun(example, pattern, shared) {
7457
8691
  }
7458
8692
  };
7459
8693
  include(example.block);
7460
- for (const block of always2) {
8694
+ for (const block of always3) {
7461
8695
  include(block);
7462
8696
  }
7463
8697
  const valueNames = /* @__PURE__ */ new Set([
@@ -7565,7 +8799,7 @@ function exampleCalls(run, inherited) {
7565
8799
  }
7566
8800
  function testCaseUnits(root, patterns, options) {
7567
8801
  return patterns.flatMap((pattern) => {
7568
- if (!(0, import_extractor7.matchesTestFileName)(options.absoluteFile, pattern.filePatterns) || !(0, import_extractor7.isListedTestFile)(options.absoluteFile, pattern.files)) {
8802
+ if (!(0, import_extractor9.matchesTestFileName)(options.absoluteFile, pattern.filePatterns) || !(0, import_extractor9.isListedTestFile)(options.absoluteFile, pattern.files)) {
7569
8803
  return [];
7570
8804
  }
7571
8805
  return examplesIn(root, options.absoluteFile, pattern, options.shared).map(
@@ -7638,7 +8872,7 @@ function testUnitOf(parts) {
7638
8872
  const range = rangeOf(anchor);
7639
8873
  return {
7640
8874
  identity: {
7641
- name: (0, import_behavioral_ir4.testUnitName)(titles.map((title) => title.text)),
8875
+ name: (0, import_behavioral_ir5.testUnitName)(titles.map((title) => title.text)),
7642
8876
  nameKind: "label",
7643
8877
  kind: "test",
7644
8878
  file: options.filePath,
@@ -7684,7 +8918,7 @@ function titleOf(call, facts) {
7684
8918
  if (first.type === "constant" || first.type === "scope_resolution") {
7685
8919
  return { text: first.text, unresolved: false };
7686
8920
  }
7687
- const read = (0, import_values16.literalOf)(evaluatedValue(first, facts));
8921
+ const read = (0, import_values17.literalOf)(evaluatedValue(first, facts));
7688
8922
  return read === null ? { text: first.text, unresolved: true } : { text: read, unresolved: false };
7689
8923
  }
7690
8924
  function isSkipped(example, pattern) {
@@ -7825,18 +9059,18 @@ function moduleOfConstant(node, options) {
7825
9059
  const key = classBehind(facts, options.absoluteFile, node);
7826
9060
  return key === void 0 ? null : options.displayPathOf((0, import_resolution10.fileOfKey)(key));
7827
9061
  }
7828
- const name = (0, import_values16.literalOf)(evaluatedValue(node, facts));
9062
+ const name = (0, import_values17.literalOf)(evaluatedValue(node, facts));
7829
9063
  const keys = name === null ? [] : facts.lookup("rbConstantName", 1, name.replace(/^::/, "")).map((row) => String(row[0]));
7830
9064
  const files = [...new Set(keys.map(import_resolution10.fileOfKey))];
7831
9065
  return files.length === 1 ? options.displayPathOf(files[0]) : null;
7832
9066
  }
7833
- function oneLine(text2) {
7834
- return text2.replace(/\s*\n\s*/g, " ").trim();
9067
+ function oneLine(text3) {
9068
+ return text3.replace(/\s*\n\s*/g, " ").trim();
7835
9069
  }
7836
9070
 
7837
9071
  // src/testClasses.ts
7838
- var import_extractor8 = require("@suss/extractor");
7839
- var import_values18 = require("@suss/values");
9072
+ var import_extractor10 = require("@suss/extractor");
9073
+ var import_values19 = require("@suss/values");
7840
9074
  var BLOCK_TYPES7 = /* @__PURE__ */ new Set(["block", "do_block"]);
7841
9075
  function testClassPatternsIn(packs) {
7842
9076
  return packs.flatMap((pack) => pack.testClasses ?? []);
@@ -7879,7 +9113,7 @@ function testClassUnits(root, patterns, options) {
7879
9113
  });
7880
9114
  }
7881
9115
  function readsFile(file, pattern) {
7882
- return (0, import_extractor8.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor8.isListedTestFile)(file, pattern.files);
9116
+ return (0, import_extractor10.matchesTestFileName)(file, pattern.filePatterns) && (0, import_extractor10.isListedTestFile)(file, pattern.files);
7883
9117
  }
7884
9118
  function testClassesIn(root, file, pattern, facts) {
7885
9119
  const found = [];
@@ -7925,7 +9159,7 @@ function testBlockOf(statement, pattern) {
7925
9159
  }
7926
9160
  function descriptionOf(call, facts) {
7927
9161
  const first = firstArgument(call);
7928
- return first === null ? null : (0, import_values18.literalOf)(evaluatedValue(first, facts));
9162
+ return first === null ? null : (0, import_values19.literalOf)(evaluatedValue(first, facts));
7929
9163
  }
7930
9164
  function anchorOf2(node) {
7931
9165
  return BLOCK_TYPES7.has(node.type) ? node.parent ?? node : node;
@@ -8102,6 +9336,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
8102
9336
  },
8103
9337
  lookup: {
8104
9338
  root: pattern.root,
9339
+ autoloadRoots: pattern.autoloadRoots ?? [],
8105
9340
  pathConvention: pattern.pathConvention,
8106
9341
  acronyms: pattern.acronyms ?? [],
8107
9342
  ancestryRootClassNames: pattern.ancestryRootClassNames,
@@ -8210,7 +9445,7 @@ function unitsFor(pattern, pack, info, ownBlocks, fileBlocks, options) {
8210
9445
  graphqlObjectFields: (p) => graphqlObjectFieldUnits(p, pack, info, ownBlocks, fileBlocks, options),
8211
9446
  controllerActions: (p) => controllerActionUnits(p, pack, info, ownBlocks, fileBlocks, options)
8212
9447
  };
8213
- return (0, import_behavioral_ir5.dispatchByType)(table, pattern);
9448
+ return (0, import_behavioral_ir6.dispatchByType)(table, pattern);
8214
9449
  }
8215
9450
  async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
8216
9451
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
@@ -8257,6 +9492,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8257
9492
  }
8258
9493
  const lookup = {
8259
9494
  root: pattern.root,
9495
+ autoloadRoots: pattern.autoloadRoots ?? [],
8260
9496
  pathConvention: pattern.pathConvention,
8261
9497
  acronyms: pattern.acronyms ?? [],
8262
9498
  ancestryRootClassNames: pattern.ancestryRootClassNames,
@@ -8271,11 +9507,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8271
9507
  ])) {
8272
9508
  return [];
8273
9509
  }
8274
- const filters = controllerFilters(pattern, ancestry, {
9510
+ const chain = controllerFilterChain(pattern, ancestry, {
8275
9511
  facts: options.facts,
8276
9512
  bodyBlocks: options.bodyBlocks,
8277
9513
  dynamicNames: options.dynamicNames
8278
9514
  });
9515
+ const filters = chain.flatMap((link) => "read" in link ? [link.read] : []);
8279
9516
  const units = [];
8280
9517
  const actions = controllerActions(
8281
9518
  pattern,
@@ -8295,15 +9532,41 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8295
9532
  options.facts,
8296
9533
  options.storage
8297
9534
  );
9535
+ const respondingHelper = respondingHelpersOf(ancestry, pattern, options);
9536
+ const exceptions = new ExceptionReader(
9537
+ lookup,
9538
+ pattern.libraryExceptions ?? {}
9539
+ );
9540
+ const caught = await handlerClasses(filters, exceptions);
9541
+ const handlersAround = (actionName) => filters.filter((filter) => filterCoversAction(filter, actionName)).flatMap((filter) => {
9542
+ const classes = caught.get(filter);
9543
+ return classes === void 0 ? [] : [classes];
9544
+ });
8298
9545
  for (const filter of filters) {
8299
- const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
8300
- const raw = filterUnit(
8301
- filter,
8302
- pattern,
8303
- displayPath,
8304
- bodyOfMethod(filter.method, filter.file, options),
8305
- options.facts
8306
- );
9546
+ const displayPath2 = options.displayPathOf?.(filter.file) ?? filter.file;
9547
+ const body = bodyOfMethod(filter.method, filter.file, options);
9548
+ const handler = filter.filter.onThrow === true;
9549
+ const raw = filterUnit(filter, pattern, displayPath2, body, {
9550
+ facts: options.facts,
9551
+ respondingHelper,
9552
+ ...handler ? {} : {
9553
+ raises: {
9554
+ classes: await raisedClasses(
9555
+ filter.method,
9556
+ filter.nesting,
9557
+ exceptions
9558
+ ),
9559
+ handlers: "eachRoute"
9560
+ },
9561
+ raised: raisedStatusBranches(
9562
+ pattern,
9563
+ options.storage?.patterns ?? [],
9564
+ body.extraEffects,
9565
+ "eachRoute",
9566
+ rangeOf(filter.method)
9567
+ )
9568
+ }
9569
+ });
8307
9570
  units.push(raw);
8308
9571
  options.onReachSeed?.(raw, {
8309
9572
  file: filter.file,
@@ -8311,38 +9574,71 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8311
9574
  enclosingQualifiedName: filter.enclosingQualifiedName
8312
9575
  });
8313
9576
  }
8314
- const emitAction = (actionName, method, block) => {
8315
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
8316
- (filter) => filterReference(
8317
- filter,
8318
- options.displayPathOf?.(filter.file) ?? filter.file
8319
- )
8320
- );
8321
- const raw = buildControllerActionUnit(
8322
- pack,
8323
- pattern,
8324
- info.qualifiedName,
8325
- actionName,
8326
- method,
8327
- {
8328
- display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
8329
- absolute: block.file
8330
- },
8331
- options,
8332
- around
8333
- );
8334
- units.push(raw);
8335
- options.onReachSeed?.(raw, {
8336
- file: block.file,
8337
- node: method,
8338
- enclosingQualifiedName: block.info.qualifiedName
8339
- });
9577
+ const displayPath = (file) => options.displayPathOf?.(file) ?? file;
9578
+ const referenceTo = (link) => "read" in link ? filterReference(
9579
+ link.read,
9580
+ displayPath(link.read.file),
9581
+ caught.get(link.read)
9582
+ ) : unreadFilterReference(link.unread, displayPath(link.unread.declaredIn));
9583
+ const emitAction = async (actionName, method, block, routes) => {
9584
+ const around = chain.filter((link) => filterCoversAction(filterOf(link), actionName)).map((link) => referenceTo(link));
9585
+ const raises2 = {
9586
+ classes: await raisedClasses(method, block.info.bodyNesting, exceptions),
9587
+ handlers: handlersAround(actionName)
9588
+ };
9589
+ for (const route of routes.length === 0 ? [null] : routes) {
9590
+ const raw = buildControllerActionUnit(
9591
+ pack,
9592
+ pattern,
9593
+ info.qualifiedName,
9594
+ actionName,
9595
+ route,
9596
+ method,
9597
+ {
9598
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
9599
+ absolute: block.file
9600
+ },
9601
+ options,
9602
+ around,
9603
+ respondingHelper,
9604
+ raises2
9605
+ );
9606
+ units.push(raw);
9607
+ options.onReachSeed?.(raw, {
9608
+ file: block.file,
9609
+ node: method,
9610
+ enclosingQualifiedName: block.info.qualifiedName
9611
+ });
9612
+ }
8340
9613
  };
8341
9614
  for (const [actionName, method, block] of actions) {
8342
- emitAction(actionName, method, block);
9615
+ const routes = pattern.routesFor(info.qualifiedName, actionName);
9616
+ await emitAction(actionName, method, block, routes);
8343
9617
  }
8344
9618
  return units;
8345
9619
  }
9620
+ async function handlerClasses(filters, exceptions) {
9621
+ const caught = /* @__PURE__ */ new Map();
9622
+ for (const filter of filters) {
9623
+ if (filter.filter.onThrow !== true) {
9624
+ continue;
9625
+ }
9626
+ const classes = [];
9627
+ for (const ref of filter.rescues.refs) {
9628
+ classes.push(await exceptions.read(ref));
9629
+ }
9630
+ caught.set(filter, { classes, someUnread: filter.rescues.someUnread });
9631
+ }
9632
+ return caught;
9633
+ }
9634
+ async function raisedClasses(method, nesting, exceptions) {
9635
+ const classes = new NodeMap();
9636
+ for (const call of escapingRaises(method)) {
9637
+ const ref = raisedClassRef(call, nesting);
9638
+ classes.set(call, ref === null ? null : await exceptions.read(ref));
9639
+ }
9640
+ return classes;
9641
+ }
8346
9642
  function controllerActions(pattern, info, ownBlocks, ancestry, options) {
8347
9643
  const actions = publicInstanceMethods(ownBlocks, options.bodyBlocks);
8348
9644
  const seen = new Set(actions.map(([actionName]) => actionName));
@@ -8359,7 +9655,7 @@ function controllerActions(pattern, info, ownBlocks, ancestry, options) {
8359
9655
  continue;
8360
9656
  }
8361
9657
  seen.add(actionName);
8362
- if (pattern.routeFor(info.qualifiedName, actionName) !== null) {
9658
+ if (pattern.routesFor(info.qualifiedName, actionName).length > 0) {
8363
9659
  actions.push(action);
8364
9660
  }
8365
9661
  }
@@ -8384,16 +9680,52 @@ function publicInstanceMethods(blocks, bodyBlocks) {
8384
9680
  }
8385
9681
  return found;
8386
9682
  }
8387
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
9683
+ function respondingHelpersOf(ancestry, pattern, options) {
9684
+ const methods = /* @__PURE__ */ new Map();
9685
+ const known = /* @__PURE__ */ new Map();
9686
+ const declared = new Set(
9687
+ (pattern.responseStatusCalls ?? []).map((call) => call.name)
9688
+ );
9689
+ const methodNamed = (name) => {
9690
+ if (!methods.has(name)) {
9691
+ const found = methodInAncestry(ancestry, name, {
9692
+ facts: options.facts,
9693
+ bodyBlocks: options.bodyBlocks,
9694
+ dynamicNames: options.dynamicNames
9695
+ });
9696
+ methods.set(name, found.type === "found" ? found.method : null);
9697
+ }
9698
+ return methods.get(name) ?? null;
9699
+ };
9700
+ return (name, call) => {
9701
+ const method = declared.has(name) ? null : methodNamed(name);
9702
+ if (method === null) {
9703
+ return null;
9704
+ }
9705
+ const bound = boundStatusArguments(method, call, pattern, options.facts);
9706
+ const key = `${name}(${[...bound].join(",")})`;
9707
+ if (!known.has(key)) {
9708
+ known.set(key, helperResponse(method, pattern, options.facts, bound));
9709
+ }
9710
+ return known.get(key) ?? null;
9711
+ };
9712
+ }
9713
+ function servesFormat(pattern, route) {
9714
+ return pattern.optionalFormat === true && route.formatless !== true && !route.path.endsWith("/");
9715
+ }
9716
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, route, method, file, bodyRead, wrappers, respondingHelper, raises2) {
8388
9717
  const range = rangeOf(method);
8389
- const route = pattern.routeFor(controllerQualifiedName, actionName);
8390
9718
  const body = bodyOfMethod(method, file.absolute, bodyRead);
8391
9719
  const perResponse = responseBranches(
8392
9720
  method,
8393
9721
  pattern,
8394
9722
  body.effects ?? [],
8395
9723
  body.extraEffects,
8396
- { facts: bodyRead.facts }
9724
+ {
9725
+ facts: bodyRead.facts,
9726
+ respondingHelper,
9727
+ raises: raises2
9728
+ }
8397
9729
  );
8398
9730
  return {
8399
9731
  identity: {
@@ -8406,11 +9738,12 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8406
9738
  exportName: actionName,
8407
9739
  exportPath: [controllerQualifiedName, actionName]
8408
9740
  },
8409
- boundaryBinding: route === null ? null : (0, import_behavioral_ir5.restBinding)({
9741
+ boundaryBinding: route === null ? null : (0, import_behavioral_ir6.restBinding)({
8410
9742
  transport: pack.protocol,
8411
9743
  method: route.method,
8412
9744
  path: route.path,
8413
- recognition: pack.name
9745
+ recognition: pack.name,
9746
+ optionalFormat: servesFormat(pattern, route)
8414
9747
  }),
8415
9748
  parameters: [],
8416
9749
  branches: withSlotSources(
@@ -8430,7 +9763,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8430
9763
  location: range
8431
9764
  },
8432
9765
  statusCodeReading: {
8433
- reading: import_extractor9.absentReading,
9766
+ reading: import_extractor11.absentReading,
8434
9767
  libraryDefault: pattern.defaultStatusCode
8435
9768
  },
8436
9769
  effects: body.effects ?? [],
@@ -8440,6 +9773,14 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8440
9773
  }
8441
9774
  ],
8442
9775
  body.provenance
9776
+ ).concat(
9777
+ raisedStatusBranches(
9778
+ pattern,
9779
+ bodyRead.storage?.patterns ?? [],
9780
+ body.extraEffects,
9781
+ raises2.handlers,
9782
+ range
9783
+ )
8443
9784
  ),
8444
9785
  bodyContent: body.bodyContent ?? "absent",
8445
9786
  dependencyCalls: [],
@@ -8481,7 +9822,7 @@ function routingGapUnit(pattern, gaps) {
8481
9822
  isDefault: true
8482
9823
  }
8483
9824
  ],
8484
- readings: gaps.map((gap) => (0, import_extractor9.unreadableReading)(gap, range)),
9825
+ readings: gaps.map((gap) => (0, import_extractor11.unreadableReading)(gap, range)),
8485
9826
  dependencyCalls: [],
8486
9827
  declaredContract: null
8487
9828
  };
@@ -8554,7 +9895,7 @@ var NO_METHOD_BEHIND_IT = {
8554
9895
  readings: []
8555
9896
  };
8556
9897
  function methodNotSettled(reason, range) {
8557
- return { readings: [(0, import_extractor9.unreadableReading)(reason, range)] };
9898
+ return { readings: [(0, import_extractor11.unreadableReading)(reason, range)] };
8558
9899
  }
8559
9900
  function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
8560
9901
  const table = {
@@ -8572,7 +9913,7 @@ function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
8572
9913
  ),
8573
9914
  none: () => nothingThere
8574
9915
  };
8575
- return (0, import_behavioral_ir5.dispatchByType)(table, found);
9916
+ return (0, import_behavioral_ir6.dispatchByType)(table, found);
8576
9917
  }
8577
9918
  async function readFieldCall(stmt, scope, ctx, ancestry) {
8578
9919
  if (stmt.type !== "call" || field(stmt, "receiver") !== null) {
@@ -8615,7 +9956,7 @@ function computedNameDeclaration(stmt, nameArg) {
8615
9956
  contract: null,
8616
9957
  body: {
8617
9958
  readings: [
8618
- (0, import_extractor9.unreadableReading)(
9959
+ (0, import_extractor11.unreadableReading)(
8619
9960
  `This field is named by ${nameArg.text}, which is worked out when the class body runs, so the name the schema exposes it under was not read here`,
8620
9961
  range
8621
9962
  )
@@ -8846,7 +10187,7 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
8846
10187
  exportName: null,
8847
10188
  exportPath: null
8848
10189
  },
8849
- boundaryBinding: decl.namedOnTheWire ? (0, import_behavioral_ir5.graphqlResolverBinding)({
10190
+ boundaryBinding: decl.namedOnTheWire ? (0, import_behavioral_ir6.graphqlResolverBinding)({
8850
10191
  transport: pack.protocol,
8851
10192
  recognition: pack.name,
8852
10193
  typeName,
@@ -9262,15 +10603,15 @@ function parseWith(language, source) {
9262
10603
  // src/project.ts
9263
10604
  var import_node_fs3 = __toESM(require("fs"), 1);
9264
10605
  var import_node_path4 = __toESM(require("path"), 1);
9265
- var import_behavioral_ir8 = require("@suss/behavioral-ir");
10606
+ var import_behavioral_ir9 = require("@suss/behavioral-ir");
9266
10607
  var import_datalog3 = require("@suss/datalog");
9267
- var import_extractor17 = require("@suss/extractor");
10608
+ var import_extractor19 = require("@suss/extractor");
9268
10609
  var import_resolution16 = require("@suss/resolution");
9269
10610
 
9270
10611
  // src/diagnostics.ts
9271
- var import_extractor10 = require("@suss/extractor");
10612
+ var import_extractor12 = require("@suss/extractor");
9272
10613
  function buildRubyExtractionReport(args) {
9273
- return (0, import_extractor10.buildUngatedExtractionReport)({
10614
+ return (0, import_extractor12.buildUngatedExtractionReport)({
9274
10615
  packs: args.packs.map((pack) => ({
9275
10616
  name: pack.name,
9276
10617
  version: pack.version ?? null,
@@ -9283,9 +10624,9 @@ function buildRubyExtractionReport(args) {
9283
10624
  }
9284
10625
 
9285
10626
  // src/exits.ts
9286
- var import_extractor11 = require("@suss/extractor");
10627
+ var import_extractor13 = require("@suss/extractor");
9287
10628
  var import_resolution11 = require("@suss/resolution");
9288
- var import_values21 = require("@suss/values");
10629
+ var import_values22 = require("@suss/values");
9289
10630
  var EXIT_METHODS = /* @__PURE__ */ new Map([
9290
10631
  ["exit", 0],
9291
10632
  ["exit!", 1],
@@ -9294,14 +10635,14 @@ var EXIT_METHODS = /* @__PURE__ */ new Map([
9294
10635
  var DEFERRED_BODY_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
9295
10636
  function exitSites(root) {
9296
10637
  const found = [];
9297
- (0, import_extractor11.walkDescendants)(root, null, {
10638
+ (0, import_extractor13.walkDescendants)(root, null, children, {
9298
10639
  at: (node) => {
9299
10640
  const site = node.type === "call" ? exitSiteAt(node) : null;
9300
10641
  if (site !== null) {
9301
10642
  found.push(site);
9302
10643
  }
9303
10644
  },
9304
- into: (node) => DEFERRED_BODY_TYPES3.has(node.type) ? import_extractor11.SKIP_CHILDREN : null
10645
+ into: (node) => DEFERRED_BODY_TYPES3.has(node.type) ? import_extractor13.SKIP_CHILDREN : null
9305
10646
  });
9306
10647
  return found;
9307
10648
  }
@@ -9338,7 +10679,7 @@ function exitCodeOf(site, facts) {
9338
10679
  if (written !== void 0) {
9339
10680
  return { type: "literal", value: written };
9340
10681
  }
9341
- const value = (0, import_values21.constantOf)(evaluatedValue(code, facts));
10682
+ const value = (0, import_values22.constantOf)(evaluatedValue(code, facts));
9342
10683
  return typeof value === "number" ? { type: "literal", value } : { type: "dynamic", sourceText: code.text };
9343
10684
  }
9344
10685
  function exitingBranches(method, effects, facts) {
@@ -9359,7 +10700,7 @@ function markExitCodeFunctions(summaries, exits, facts, workspaceRoot) {
9359
10700
  (site) => site.code === null ? [] : [nodeId(file, site.code)]
9360
10701
  )
9361
10702
  );
9362
- (0, import_extractor11.markReturnsAsExitCode)(
10703
+ (0, import_extractor13.markReturnsAsExitCode)(
9363
10704
  summaries,
9364
10705
  (0, import_resolution11.exitCodeFunctions)(facts, sinks),
9365
10706
  workspaceRoot
@@ -9409,9 +10750,9 @@ function eventsOf(args, called, callbacks, db) {
9409
10750
 
9410
10751
  // src/facts.ts
9411
10752
  var import_node_path3 = __toESM(require("path"), 1);
9412
- var import_extractor12 = require("@suss/extractor");
10753
+ var import_extractor14 = require("@suss/extractor");
9413
10754
  function emitRequireFacts(db, filePath, root, known) {
9414
- (0, import_extractor12.walkDescendants)(root, null, {
10755
+ (0, import_extractor14.walkDescendants)(root, null, children, {
9415
10756
  at: (node) => {
9416
10757
  const target = requiredRelativePath(node);
9417
10758
  if (target === null) {
@@ -9425,7 +10766,7 @@ function emitRequireFacts(db, filePath, root, known) {
9425
10766
  db.add("importsFile", [filePath, resolved]);
9426
10767
  }
9427
10768
  },
9428
- into: (node) => node.type === "argument_list" ? import_extractor12.SKIP_CHILDREN : null
10769
+ into: (node) => node.type === "argument_list" ? import_extractor14.SKIP_CHILDREN : null
9429
10770
  });
9430
10771
  }
9431
10772
  function requiredRelativePath(node) {
@@ -9437,17 +10778,17 @@ function requiredRelativePath(node) {
9437
10778
  }
9438
10779
 
9439
10780
  // src/moduleSurface.ts
9440
- var import_behavioral_ir7 = require("@suss/behavioral-ir");
9441
- var import_extractor15 = require("@suss/extractor");
10781
+ var import_behavioral_ir8 = require("@suss/behavioral-ir");
10782
+ var import_extractor17 = require("@suss/extractor");
9442
10783
 
9443
10784
  // src/reach/closure.ts
9444
- var import_behavioral_ir6 = require("@suss/behavioral-ir");
10785
+ var import_behavioral_ir7 = require("@suss/behavioral-ir");
9445
10786
  var import_datalog2 = require("@suss/datalog");
9446
- var import_extractor14 = require("@suss/extractor");
10787
+ var import_extractor16 = require("@suss/extractor");
9447
10788
  var import_resolution13 = require("@suss/resolution");
9448
10789
 
9449
10790
  // src/flags.ts
9450
- var import_extractor13 = require("@suss/extractor");
10791
+ var import_extractor15 = require("@suss/extractor");
9451
10792
  var PARSE_METHODS = /* @__PURE__ */ new Set(["parse", "parse!", "permute", "permute!"]);
9452
10793
  var DEFERRED_BODY_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
9453
10794
  function optionParserFlagReads(method, parameterNames3) {
@@ -9476,9 +10817,9 @@ function optionParserFlagReads(method, parameterNames3) {
9476
10817
  return reads;
9477
10818
  }
9478
10819
  function walkBody2(body, at) {
9479
- (0, import_extractor13.walkDescendants)(body, null, {
10820
+ (0, import_extractor15.walkDescendants)(body, null, children, {
9480
10821
  at,
9481
- into: (node) => DEFERRED_BODY_TYPES4.has(node.type) ? import_extractor13.SKIP_CHILDREN : null
10822
+ into: (node) => DEFERRED_BODY_TYPES4.has(node.type) ? import_extractor15.SKIP_CHILDREN : null
9482
10823
  });
9483
10824
  }
9484
10825
  var OPTION_PARSER_SPELLINGS = /* @__PURE__ */ new Set(["OptionParser", "::OptionParser"]);
@@ -9492,7 +10833,7 @@ function flagsDeclaredIn(construction) {
9492
10833
  return [];
9493
10834
  }
9494
10835
  const flags = [];
9495
- (0, import_extractor13.walkDescendants)(block, null, {
10836
+ (0, import_extractor15.walkDescendants)(block, null, children, {
9496
10837
  at: (node) => {
9497
10838
  if (node.type !== "call" || field(node, "method")?.text !== "on") {
9498
10839
  return;
@@ -9507,8 +10848,8 @@ function flagsDeclaredIn(construction) {
9507
10848
  return flags;
9508
10849
  }
9509
10850
  function flagOf(args) {
9510
- const written = args.map((arg) => stringLiteralValue(arg)).filter((text2) => text2 !== null && text2.startsWith("-"));
9511
- const chosen = written.find((text2) => text2.startsWith("--")) ?? written[0] ?? null;
10851
+ const written = args.map((arg) => stringLiteralValue(arg)).filter((text3) => text3 !== null && text3.startsWith("-"));
10852
+ const chosen = written.find((text3) => text3.startsWith("--")) ?? written[0] ?? null;
9512
10853
  if (chosen === null) {
9513
10854
  return null;
9514
10855
  }
@@ -9546,8 +10887,6 @@ function parserCalledOn(receiver, parserNamed) {
9546
10887
 
9547
10888
  // src/reach/resolveCallee.ts
9548
10889
  var import_resolution12 = require("@suss/resolution");
9549
- var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
9550
- var INVOKES_RECEIVER = "call";
9551
10890
  var STOP_FOR = {
9552
10891
  severalSources: "multipleSources",
9553
10892
  outsideRun: "outsideRun",
@@ -9564,6 +10903,11 @@ var UNSETTLED_ON_SELF = {
9564
10903
  reason: "noDeclaration",
9565
10904
  matchByName: true
9566
10905
  };
10906
+ var SELF_CALL_IN_MIXIN = {
10907
+ kind: "stopped",
10908
+ reason: "selfCallInMixin",
10909
+ matchByName: true
10910
+ };
9567
10911
  function followed(target) {
9568
10912
  return { kind: "followed", target };
9569
10913
  }
@@ -9625,6 +10969,57 @@ function loaderSourceCallee(call, site, ctx) {
9625
10969
  }
9626
10970
  return null;
9627
10971
  }
10972
+ function dispatchedCalls(calls, ctx) {
10973
+ const byKey = new Map(
10974
+ calls.map((one) => [nodeId(one.site.file, one.call), one])
10975
+ );
10976
+ const found = /* @__PURE__ */ new Map();
10977
+ const outcomes = (0, import_resolution12.dispatchOutcomes)(ctx.facts, [...byKey.keys()], RUBY_PROGRAM);
10978
+ for (const [key, outcome] of outcomes) {
10979
+ const asked = byKey.get(key);
10980
+ if (asked === void 0) {
10981
+ continue;
10982
+ }
10983
+ const fits = sendFits(asked.call, asked.site, ctx);
10984
+ const targets = /* @__PURE__ */ new Map();
10985
+ for (const fn of outcome.functions) {
10986
+ for (const target of reachedThrough(fn, outcome.through, ctx)) {
10987
+ if (fits(target)) {
10988
+ targets.set(nodeId(target.file, target.node), target);
10989
+ }
10990
+ }
10991
+ }
10992
+ found.set(asked.call.id, {
10993
+ targets: [...targets.values()],
10994
+ certain: outcome.certain && outcome.functions.every((fn) => ctx.definitions.has(fn)),
10995
+ unresolved: outcome.unresolved
10996
+ });
10997
+ }
10998
+ return found;
10999
+ }
11000
+ function reachedThrough(key, through, ctx) {
11001
+ const own = ctx.definitions.get(key);
11002
+ if (own !== void 0) {
11003
+ return [own];
11004
+ }
11005
+ return (through.get(key) ?? []).flatMap((inner) => {
11006
+ const target = ctx.definitions.get(inner);
11007
+ return target === void 0 ? [] : [target];
11008
+ });
11009
+ }
11010
+ function sendFits(call, site, ctx) {
11011
+ const method = calleeMethodName(call);
11012
+ if (method === void 0 || !SENDING_METHODS.has(method)) {
11013
+ return () => true;
11014
+ }
11015
+ const receiver = field(call, "receiver");
11016
+ const toClass = receiver === null || receiver.type === "self" ? inClassMethod(site, ctx) : readsOffClassItself(
11017
+ receiver,
11018
+ readKey(site.file, receiver, site.method),
11019
+ classLocalsFor(call, site)
11020
+ );
11021
+ return (target) => definesClassMethod(target.node, ctx.bodyBlocks) === toClass;
11022
+ }
9628
11023
  function outcomeFor(key, ctx, read) {
9629
11024
  return read?.outcomes.get(key) ?? (0, import_resolution12.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
9630
11025
  }
@@ -9638,14 +11033,16 @@ function readsAsACall(call, site, ctx, read) {
9638
11033
  return false;
9639
11034
  }
9640
11035
  const outcome = outcomeFor(spelling.key, ctx, read);
9641
- return outcome.kind === "function" || outcome.kind === "object";
11036
+ return outcome.kind === "function" || outcome.kind === "object" || objectsBehind(ctx.facts, spelling.key).some(
11037
+ (key) => ctx.classNames.has(key)
11038
+ );
9642
11039
  }
9643
11040
  function spellingFor2(call, site) {
9644
11041
  const methodName = calleeMethodName(call);
9645
11042
  if (methodName === void 0) {
9646
11043
  return { kind: "stopped", reason: "noDeclaration" };
9647
11044
  }
9648
- if (DYNAMIC_SEND_NAMES.has(methodName)) {
11045
+ if (SENDING_METHODS.has(methodName)) {
9649
11046
  return { kind: "stopped", reason: "unsettledValue" };
9650
11047
  }
9651
11048
  const receiver = field(call, "receiver");
@@ -9691,7 +11088,7 @@ function asCallee(spelling, outcome, site, ctx) {
9691
11088
  return methodOnObject(spelling, objectKey, ctx);
9692
11089
  }
9693
11090
  if (outcome.kind === "function") {
9694
- return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
11091
+ return spelling.method === INVOKE_METHOD ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
9695
11092
  }
9696
11093
  return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
9697
11094
  }
@@ -9735,7 +11132,7 @@ function scopeOn(qualifiedName, method, ctx) {
9735
11132
  return null;
9736
11133
  }
9737
11134
  function fromCaller(spelling, parameterKey, site) {
9738
- if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKES_RECEIVER) {
11135
+ if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKE_METHOD) {
9739
11136
  return stop("unsettledValue");
9740
11137
  }
9741
11138
  return stop("callerSupplied");
@@ -9752,22 +11149,27 @@ function leavesRoomForATopLevelMethod(reason) {
9752
11149
  return reason === "noDeclaration" || reason === "outsideRun";
9753
11150
  }
9754
11151
  function resolveImplicitSelf(methodName, site, ctx) {
9755
- if (site.enclosingQualifiedName !== null) {
9756
- const onClass = methodOnSelf(
9757
- site,
9758
- site.enclosingQualifiedName,
9759
- methodName,
9760
- ctx
9761
- );
9762
- if (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason)) {
9763
- return onClass;
9764
- }
11152
+ const onClass = site.enclosingQualifiedName === null ? null : methodOnSelf(site, site.enclosingQualifiedName, methodName, ctx);
11153
+ if (onClass !== null && (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason))) {
11154
+ return onClass;
9765
11155
  }
11156
+ const readWholeAncestry = onClass?.kind === "stopped" && onClass.reason === "noDeclaration";
9766
11157
  const topLevel = resolveTopLevelName(methodName, ctx);
9767
11158
  if (topLevel.kind === "followed" || topLevel.reason !== "noDeclaration") {
9768
11159
  return topLevel;
9769
11160
  }
9770
- return declaredForTheOtherSelf(site, methodName, ctx) ? NO_DECLARATION : UNSETTLED_ON_SELF;
11161
+ if (declaredForTheOtherSelf(site, methodName, ctx)) {
11162
+ return NO_DECLARATION;
11163
+ }
11164
+ return readWholeAncestry && !OBJECT_METHODS.has(methodName) && inModuleInstanceMethod(site, ctx) ? SELF_CALL_IN_MIXIN : UNSETTLED_ON_SELF;
11165
+ }
11166
+ function inModuleInstanceMethod(site, ctx) {
11167
+ const qualifiedName = site.enclosingQualifiedName;
11168
+ if (qualifiedName === null || site.method === null || inClassMethod(site, ctx)) {
11169
+ return false;
11170
+ }
11171
+ const own = ctx.ancestries.get(qualifiedName)?.[0];
11172
+ return own?.type === "bodies" && own.blocks.every((block) => block.info.kind === "module");
9771
11173
  }
9772
11174
  function declaredForTheOtherSelf(site, methodName, ctx) {
9773
11175
  const qualifiedName = site.enclosingQualifiedName;
@@ -9810,12 +11212,16 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
9810
11212
  return followed(reachedMethod(found.method, found.block, methodName));
9811
11213
  }
9812
11214
  if (found.type === "unsettled") {
9813
- return stop(
9814
- found.cause === "dynamicDefine" ? "definedAtLoadTime" : "outsideRun"
9815
- );
11215
+ return stop(unsettledReason(found));
9816
11216
  }
9817
11217
  return NO_DECLARATION;
9818
11218
  }
11219
+ function unsettledReason(found) {
11220
+ if (found.cause === "dynamicDefine") {
11221
+ return "definedAtLoadTime";
11222
+ }
11223
+ return found.definedLater === true ? "behindUnreadAncestor" : "outsideRun";
11224
+ }
9819
11225
  function reachedMethod(node, block, name) {
9820
11226
  return {
9821
11227
  file: block.file,
@@ -9862,6 +11268,7 @@ async function reachedFunctions(seeds, options) {
9862
11268
  const targetsByKey = /* @__PURE__ */ new Map();
9863
11269
  const stopsByKey = /* @__PURE__ */ new Map();
9864
11270
  const argTargetsByKey = /* @__PURE__ */ new Map();
11271
+ const possibleTargetsByKey = /* @__PURE__ */ new Map();
9865
11272
  const parameterCallsByKey = /* @__PURE__ */ new Map();
9866
11273
  const propertyReadsByKey = /* @__PURE__ */ new Map();
9867
11274
  const receiverClassesByKey = /* @__PURE__ */ new Map();
@@ -9920,6 +11327,7 @@ async function reachedFunctions(seeds, options) {
9920
11327
  }
9921
11328
  targetsByKey.set(key, scan.targets);
9922
11329
  argTargetsByKey.set(key, scan.argTargets);
11330
+ possibleTargetsByKey.set(key, scan.possibleTargets);
9923
11331
  if (scan.propertyReads.size > 0) {
9924
11332
  propertyReadsByKey.set(key, scan.propertyReads);
9925
11333
  }
@@ -9975,15 +11383,16 @@ async function reachedFunctions(seeds, options) {
9975
11383
  const build = () => {
9976
11384
  const raw = libraryUnit(target, options);
9977
11385
  dropPropertyReads(raw, propertyReadsByKey.get(key));
9978
- const summary2 = (0, import_extractor14.assembleSummary)(raw, { gapHandling });
11386
+ const summary2 = (0, import_extractor16.assembleSummary)(raw, { gapHandling });
9979
11387
  if (gapHandling !== "silent") {
9980
11388
  summary2.gaps.push(
9981
- ...(stopsByKey.get(key) ?? []).map(import_behavioral_ir6.unfollowedCallGap)
11389
+ ...(stopsByKey.get(key) ?? []).map(import_behavioral_ir7.unfollowedCallGap)
9982
11390
  );
9983
11391
  }
9984
- (0, import_behavioral_ir6.placeCalls)(summary2, targetsByKey.get(key));
9985
- (0, import_behavioral_ir6.placeArgTargets)(summary2, argTargetsByKey.get(key));
9986
- (0, import_behavioral_ir6.placeCalleeParameters)(summary2, parameterCallsByKey.get(key));
11392
+ (0, import_behavioral_ir7.placeCalls)(summary2, targetsByKey.get(key));
11393
+ (0, import_behavioral_ir7.placeArgTargets)(summary2, argTargetsByKey.get(key));
11394
+ (0, import_behavioral_ir7.placePossibleCalls)(summary2, possibleTargetsByKey.get(key));
11395
+ (0, import_behavioral_ir7.placeCalleeParameters)(summary2, parameterCallsByKey.get(key));
9987
11396
  return summary2;
9988
11397
  };
9989
11398
  const summary = ledger === void 0 ? build() : ledger.charging(chargeOf(charges, key), () => {
@@ -9998,13 +11407,14 @@ async function reachedFunctions(seeds, options) {
9998
11407
  summaries.push(summary);
9999
11408
  }
10000
11409
  if (gapHandling !== "silent") {
10001
- (0, import_behavioral_ir6.recordParameterGaps)(parameterCallsByKey, summariesByKey, passedPositions);
11410
+ (0, import_behavioral_ir7.recordParameterGaps)(parameterCallsByKey, summariesByKey, passedPositions);
10002
11411
  }
10003
11412
  return {
10004
11413
  summaries,
10005
11414
  targetsByKey,
10006
11415
  stopsByKey,
10007
11416
  argTargetsByKey,
11417
+ possibleTargetsByKey,
10008
11418
  parameterCallsByKey,
10009
11419
  propertyReadsByKey,
10010
11420
  receiverClassesByKey,
@@ -10034,36 +11444,70 @@ function batchedReads(round, options) {
10034
11444
  const bodies = round.flatMap(
10035
11445
  (entry) => "source" in entry ? [{ key: entry.key, ...bodyOf2(entry.source, options) }] : []
10036
11446
  );
10037
- const spellings = calleeSpellings(
10038
- bodies.flatMap((body) => body.written),
10039
- options.context
11447
+ const written = bodies.flatMap((body) => body.written);
11448
+ const spellings = calleeSpellings(written, options.context);
11449
+ const dispatched = dispatchedAmong(written, spellings, options.context);
11450
+ return new Map(
11451
+ bodies.map((body) => [body.key, { ...body, spellings, dispatched }])
11452
+ );
11453
+ }
11454
+ function dispatchedAmong(written, spellings, ctx) {
11455
+ return dispatchedCalls(
11456
+ written.filter(({ call, site }) => {
11457
+ if (!mayRunAnEntry(call)) {
11458
+ return false;
11459
+ }
11460
+ const outcome = resolveCallee(call, site, ctx, spellings);
11461
+ return outcome.kind === "stopped" && outcome.reason !== "callerSupplied";
11462
+ }),
11463
+ ctx
10040
11464
  );
10041
- return new Map(bodies.map((body) => [body.key, { ...body, spellings }]));
11465
+ }
11466
+ function mayRunAnEntry(call) {
11467
+ const sent = calleeMethodName(call);
11468
+ if (sent !== void 0 && SENDING_METHODS.has(sent)) {
11469
+ return true;
11470
+ }
11471
+ if (call.type !== "call" || field(call, "receiver") === null) {
11472
+ return false;
11473
+ }
11474
+ const method = field(call, "method");
11475
+ return method === null || method.text === INVOKE_METHOD;
10042
11476
  }
10043
11477
  function scanOnce(source, key, options, read, charges) {
10044
11478
  const ledger = options.ledger;
10045
11479
  if (ledger === void 0) {
10046
- const body = read ?? {
10047
- ...bodyOf2(source, options),
10048
- spellings: { spellingOf: /* @__PURE__ */ new Map(), outcomes: /* @__PURE__ */ new Map() }
10049
- };
10050
- return scanBody(source, options.context, options, body);
11480
+ return scanBody(
11481
+ source,
11482
+ options.context,
11483
+ options,
11484
+ read ?? ownReads(source, options)
11485
+ );
10051
11486
  }
10052
11487
  return ledger.charging(chargeOf(charges, key), () => {
10053
11488
  forgetEvaluations(options.context.facts);
10054
11489
  ledger.readFile(source.file);
10055
- const body = bodyOf2(source, options);
10056
- const spellings = calleeSpellings(body.written, options.context);
10057
- const scan = scanBody(source, options.context, options, {
10058
- ...body,
10059
- spellings
10060
- });
11490
+ const scan = scanBody(
11491
+ source,
11492
+ options.context,
11493
+ options,
11494
+ ownReads(source, options)
11495
+ );
10061
11496
  for (const target of scan.followed) {
10062
11497
  ledger.touchKey(keyOf(target));
10063
11498
  }
10064
11499
  return scan;
10065
11500
  });
10066
11501
  }
11502
+ function ownReads(source, options) {
11503
+ const body = bodyOf2(source, options);
11504
+ const spellings = calleeSpellings(body.written, options.context);
11505
+ return {
11506
+ ...body,
11507
+ spellings,
11508
+ dispatched: dispatchedAmong(body.written, spellings, options.context)
11509
+ };
11510
+ }
10067
11511
  function storedScan(scan) {
10068
11512
  return {
10069
11513
  followed: scan.followed.map((target) => ({
@@ -10079,6 +11523,10 @@ function storedScan(scan) {
10079
11523
  callee,
10080
11524
  [...byPosition]
10081
11525
  ]),
11526
+ possibleTargets: [...scan.possibleTargets].map(([callee, places]) => [
11527
+ callee,
11528
+ [...places]
11529
+ ]),
10082
11530
  parameterCalls: [...scan.parameterCalls],
10083
11531
  passedPositions: [...scan.passedPositions],
10084
11532
  propertyReads: [...scan.propertyReads],
@@ -10096,6 +11544,7 @@ function scanFromStored(stored, functionFor) {
10096
11544
  new Map(byPosition)
10097
11545
  ])
10098
11546
  ),
11547
+ possibleTargets: new Map(stored.possibleTargets),
10099
11548
  parameterCalls: stored.parameterCalls,
10100
11549
  passedPositions: new Set(stored.passedPositions),
10101
11550
  propertyReads: new Set(stored.propertyReads),
@@ -10124,6 +11573,7 @@ var EMPTY_SCAN = {
10124
11573
  stops: [],
10125
11574
  targets: /* @__PURE__ */ new Map(),
10126
11575
  argTargets: /* @__PURE__ */ new Map(),
11576
+ possibleTargets: /* @__PURE__ */ new Map(),
10127
11577
  parameterCalls: [],
10128
11578
  passedPositions: /* @__PURE__ */ new Set(),
10129
11579
  propertyReads: /* @__PURE__ */ new Set(),
@@ -10145,7 +11595,7 @@ function bodyOf2(source, options) {
10145
11595
  const file = run?.fileOf(call) ?? source.file;
10146
11596
  return file === source.file ? site : { ...site, file, method: enclosingDefinition(call) };
10147
11597
  };
10148
- const written = callsWrittenIn(source.node, run, options);
11598
+ const written = callsWrittenIn2(source.node, run, options);
10149
11599
  const calls = callsReported(
10150
11600
  written,
10151
11601
  (call) => mightReadAsACall(call, siteFor(call), options.context)
@@ -10159,7 +11609,7 @@ function bodyOf2(source, options) {
10159
11609
  written: calls.map((call) => ({ call, site: siteFor(call) }))
10160
11610
  };
10161
11611
  }
10162
- function callsWrittenIn(node, run, options) {
11612
+ function callsWrittenIn2(node, run, options) {
10163
11613
  if (run !== null) {
10164
11614
  return exampleCalls(run, options.inheritedMethods).filter(
10165
11615
  (call) => !run.isRunnerCall(call)
@@ -10211,9 +11661,9 @@ function scanBody(source, ctx, options, read) {
10211
11661
  );
10212
11662
  const ownParameters = positionalParameters(source.node).map((p) => p.name);
10213
11663
  const followed2 = [];
10214
- const stops = [];
10215
- const placements = new import_behavioral_ir6.TargetPlacements();
10216
- const receiverClasses = new import_behavioral_ir6.ReceiverClasses();
11664
+ const stops = new import_behavioral_ir7.StopList();
11665
+ const placements = new import_behavioral_ir7.TargetPlacements();
11666
+ const receiverClasses = new import_behavioral_ir7.ReceiverClasses();
10217
11667
  const parameterCalls = [];
10218
11668
  const passedPositions = /* @__PURE__ */ new Set();
10219
11669
  const seen = /* @__PURE__ */ new Set();
@@ -10226,6 +11676,12 @@ function scanBody(source, ctx, options, read) {
10226
11676
  }
10227
11677
  const argNodes = bodyStatements(args);
10228
11678
  for (let position = 0; position < argNodes.length; position += 1) {
11679
+ if (argNodes[position]?.type === LAMBDA_TYPE) {
11680
+ if (calleeKey !== null) {
11681
+ passedPositions.add(`${calleeKey}#${position}`);
11682
+ }
11683
+ continue;
11684
+ }
10229
11685
  const symbol = methodReferenceSymbol(argNodes[position]);
10230
11686
  if (symbol === null) {
10231
11687
  continue;
@@ -10263,11 +11719,35 @@ function scanBody(source, ctx, options, read) {
10263
11719
  span: spanOf(target.node)
10264
11720
  });
10265
11721
  };
11722
+ const follow = (target) => {
11723
+ const key = keyOf(target);
11724
+ if (!seen.has(key)) {
11725
+ seen.add(key);
11726
+ followed2.push(target);
11727
+ }
11728
+ };
10266
11729
  for (const call of calls) {
10267
11730
  const callee = calleeText(call);
10268
11731
  const callSite = read.siteFor(call);
10269
11732
  const predicate = read.predicateOf(call);
10270
- const outcome = predicate === null ? resolveCallee(call, callSite, ctx, read.spellings) : resolveMethodOn(predicate.target, predicate.method, callSite, ctx);
11733
+ const settled = predicate === null ? resolveCallee(call, callSite, ctx, read.spellings) : resolveMethodOn(predicate.target, predicate.method, callSite, ctx);
11734
+ const dispatched = settled.kind === "stopped" ? read.dispatched.get(call.id) : void 0;
11735
+ const [only] = dispatched?.targets ?? [];
11736
+ if (dispatched !== void 0 && only !== void 0 && !dispatched.certain) {
11737
+ dispatched.targets.forEach(follow);
11738
+ placements.placePossible(
11739
+ callee,
11740
+ dispatched.targets.map((target) => ({
11741
+ file: displayPathOf(target.file),
11742
+ span: spanOf(target.node)
11743
+ }))
11744
+ );
11745
+ if (dispatched.unresolved > 0) {
11746
+ stops.add({ callee, reason: "unsettledValue" });
11747
+ }
11748
+ continue;
11749
+ }
11750
+ const outcome = only === void 0 ? settled : { kind: "followed", target: only };
10271
11751
  if (options.storage !== void 0) {
10272
11752
  for (const callback of callbacksReached(
10273
11753
  call,
@@ -10295,11 +11775,13 @@ function scanBody(source, ctx, options, read) {
10295
11775
  outcome.kind === "followed" ? keyOf(outcome.target) : null
10296
11776
  );
10297
11777
  if (outcome.kind === "stopped") {
10298
- const stopKey = `${outcome.reason}:${callee}`;
10299
11778
  const claimed = options.storage !== void 0 && storageClaims(call, callSite.file, options.storage, callSite.method);
10300
- if (!seen.has(stopKey) && !claimed && (0, import_behavioral_ir6.worthRecording)(outcome.reason)) {
10301
- seen.add(stopKey);
10302
- stops.push({ callee, reason: outcome.reason });
11779
+ if (!claimed && (0, import_behavioral_ir7.worthRecording)(outcome.reason)) {
11780
+ stops.add({
11781
+ callee,
11782
+ reason: outcome.reason,
11783
+ ...resultUsedAsValue(call) ? { usedAsValue: true } : {}
11784
+ });
10303
11785
  }
10304
11786
  if (outcome.reason === "callerSupplied" && !parameterCallsSeen.has(callee)) {
10305
11787
  const receiverName = field(call, "receiver")?.text;
@@ -10314,17 +11796,14 @@ function scanBody(source, ctx, options, read) {
10314
11796
  if (isArglessReceiverCall(call)) {
10315
11797
  keptArgless.add(call.id);
10316
11798
  }
10317
- const key = keyOf(outcome.target);
10318
- if (!seen.has(key)) {
10319
- seen.add(key);
10320
- followed2.push(outcome.target);
10321
- }
11799
+ follow(outcome.target);
10322
11800
  }
10323
11801
  return {
10324
11802
  followed: followed2,
10325
- stops,
11803
+ stops: stops.stops,
10326
11804
  targets: placements.targets,
10327
11805
  argTargets: placements.argTargets,
11806
+ possibleTargets: placements.possibleTargets,
10328
11807
  parameterCalls,
10329
11808
  passedPositions,
10330
11809
  propertyReads: propertyReadsAmong(read.argless, calls, keptArgless),
@@ -10356,14 +11835,14 @@ function propertyReadsAmong(argless, made, keptArgless) {
10356
11835
  );
10357
11836
  const reads = /* @__PURE__ */ new Set();
10358
11837
  for (const call of argless) {
10359
- const text2 = calleeText(call);
10360
- if (!kept.has(text2)) {
10361
- reads.add(text2);
11838
+ const text3 = calleeText(call);
11839
+ if (!kept.has(text3)) {
11840
+ reads.add(text3);
10362
11841
  }
10363
11842
  }
10364
11843
  return reads;
10365
11844
  }
10366
- function libraryUnit(target, options, binding = (0, import_behavioral_ir6.functionCallBinding)({
11845
+ function libraryUnit(target, options, binding = (0, import_behavioral_ir7.functionCallBinding)({
10367
11846
  transport: "in-process",
10368
11847
  recognition: "reachable"
10369
11848
  })) {
@@ -10448,10 +11927,10 @@ function parameterName(param) {
10448
11927
 
10449
11928
  // src/moduleSurface.ts
10450
11929
  function settleRubyModules(modules) {
10451
- return (0, import_extractor15.settleModules)(modules, ["{name}.rb", "../{name}.rb"]);
11930
+ return (0, import_extractor17.settleModules)(modules, ["{name}.rb", "../{name}.rb"]);
10452
11931
  }
10453
11932
  function moduleExportUnits(root, file, options) {
10454
- const module2 = (0, import_extractor15.moduleOfFile)(options.modules, file);
11933
+ const module2 = (0, import_extractor17.moduleOfFile)(options.modules, file);
10455
11934
  if (module2 === void 0 || !module2.public.includes(file)) {
10456
11935
  return [];
10457
11936
  }
@@ -10478,8 +11957,8 @@ function moduleExportUnits(root, file, options) {
10478
11957
  exportPath: [info.qualifiedName, method]
10479
11958
  },
10480
11959
  options,
10481
- (0, import_behavioral_ir7.moduleExportBinding)({
10482
- recognition: import_extractor15.MODULE_SURFACE_RECOGNITION,
11960
+ (0, import_behavioral_ir8.moduleExportBinding)({
11961
+ recognition: import_extractor17.MODULE_SURFACE_RECOGNITION,
10483
11962
  module: module2.name,
10484
11963
  exportName: name
10485
11964
  })
@@ -10787,11 +12266,11 @@ var WalkReplay = class {
10787
12266
  };
10788
12267
 
10789
12268
  // src/testFactories.ts
10790
- var import_values28 = require("@suss/values");
12269
+ var import_values29 = require("@suss/values");
10791
12270
  function factoriesIn(packs) {
10792
12271
  return packs.flatMap((pack) => pack.factories ?? []);
10793
12272
  }
10794
- function emitFactoryFacts(db, files, isTestFile, patterns, inflections) {
12273
+ function emitFactoryFacts(db, files, isTestFile2, patterns, inflections) {
10795
12274
  for (const pattern of patterns) {
10796
12275
  const definitions = /* @__PURE__ */ new Map();
10797
12276
  for (const { file, root } of files) {
@@ -10799,7 +12278,7 @@ function emitFactoryFacts(db, files, isTestFile, patterns, inflections) {
10799
12278
  }
10800
12279
  const classes = new FactoryClasses(db, definitions, inflections);
10801
12280
  for (const { file, root } of files) {
10802
- if (!isTestFile(file)) {
12281
+ if (!isTestFile2(file)) {
10803
12282
  continue;
10804
12283
  }
10805
12284
  for (const [call, name] of buildsIn(db, root, pattern)) {
@@ -10849,7 +12328,7 @@ function classRefOf(db, file, node) {
10849
12328
  if (node.type === "constant" || node.type === "scope_resolution") {
10850
12329
  return { key: readKey(file, node, null) };
10851
12330
  }
10852
- const written = (0, import_values28.literalOf)(evaluatedValue(node, db));
12331
+ const written = (0, import_values29.literalOf)(evaluatedValue(node, db));
10853
12332
  if (written === null) {
10854
12333
  return null;
10855
12334
  }
@@ -10858,7 +12337,7 @@ function classRefOf(db, file, node) {
10858
12337
  };
10859
12338
  }
10860
12339
  function nameOf(db, node) {
10861
- return symbolValue(node) ?? (0, import_values28.literalOf)(evaluatedValue(node, db));
12340
+ return symbolValue(node) ?? (0, import_values29.literalOf)(evaluatedValue(node, db));
10862
12341
  }
10863
12342
  var FactoryClasses = class {
10864
12343
  constructor(db, definitions, inflections) {
@@ -10930,7 +12409,7 @@ function builtAt(db, call, pattern) {
10930
12409
 
10931
12410
  // src/testSharedGroups.ts
10932
12411
  var import_resolution15 = require("@suss/resolution");
10933
- var import_values30 = require("@suss/values");
12412
+ var import_values31 = require("@suss/values");
10934
12413
  var ByNode = class {
10935
12414
  byTree = /* @__PURE__ */ new WeakMap();
10936
12415
  set(node, value) {
@@ -11034,7 +12513,7 @@ var SharedGroupIndex = class {
11034
12513
  }
11035
12514
  nameOf(call) {
11036
12515
  const first = firstArgument(call);
11037
- return first === null ? null : (0, import_values30.literalOf)(evaluatedValue(first, this.db));
12516
+ return first === null ? null : (0, import_values31.literalOf)(evaluatedValue(first, this.db));
11038
12517
  }
11039
12518
  noteRead(definition) {
11040
12519
  if (definition !== null) {
@@ -11111,16 +12590,16 @@ function visibleAt(index, site, pattern) {
11111
12590
  }
11112
12591
 
11113
12592
  // src/version.ts
11114
- var import_extractor16 = require("@suss/extractor");
12593
+ var import_extractor18 = require("@suss/extractor");
11115
12594
  var ADAPTER_VERSION = "0.1.0";
11116
- var adapterStamp = (0, import_extractor16.createAdapterStamp)({
12595
+ var adapterStamp = (0, import_extractor18.createAdapterStamp)({
11117
12596
  moduleUrl: importMetaUrl,
11118
12597
  version: ADAPTER_VERSION
11119
12598
  });
11120
12599
 
11121
12600
  // src/project.ts
11122
12601
  function keptRubyParses() {
11123
- return new import_extractor17.KeptParses((root) => root.tree.delete());
12602
+ return new import_extractor19.KeptParses((root) => root.tree.delete());
11124
12603
  }
11125
12604
  async function parseRubyAhead(files, kept) {
11126
12605
  for (const file of files) {
@@ -11225,7 +12704,7 @@ var RunFacts = class {
11225
12704
  ...this.testFiles.map(({ file }) => file),
11226
12705
  ...this.parsed.map(({ file }) => file).filter(
11227
12706
  (file) => classTests.some(
11228
- (one) => (0, import_extractor17.matchesTestFileName)(file, one.filePatterns)
12707
+ (one) => (0, import_extractor19.matchesTestFileName)(file, one.filePatterns)
11229
12708
  )
11230
12709
  )
11231
12710
  ]);
@@ -11260,7 +12739,7 @@ function factsForFile(options) {
11260
12739
  return facts.db;
11261
12740
  }
11262
12741
  function moduleInitUnit(options) {
11263
- const raw = (0, import_extractor17.moduleInitStructure)({
12742
+ const raw = (0, import_extractor19.moduleInitStructure)({
11264
12743
  name: options.name,
11265
12744
  file: options.file,
11266
12745
  range: options.range,
@@ -11274,7 +12753,7 @@ function moduleInitUnit(options) {
11274
12753
  }
11275
12754
  function discoveredAs(raw) {
11276
12755
  const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
11277
- const boundary = raw.boundaryBinding === null ? "" : (0, import_behavioral_ir8.boundaryKey)(raw.boundaryBinding) ?? "";
12756
+ const boundary = raw.boundaryBinding === null ? "" : (0, import_behavioral_ir9.boundaryKey)(raw.boundaryBinding) ?? "";
11278
12757
  return `${raw.identity.file}::${reported}::${raw.identity.range.start}::${boundary}`;
11279
12758
  }
11280
12759
  function replaysCleanly(units, discovered) {
@@ -11291,16 +12770,16 @@ function replaysCleanly(units, discovered) {
11291
12770
  return true;
11292
12771
  }
11293
12772
  async function extractRubyProject(options) {
11294
- const timer = options.onTiming !== void 0 ? (0, import_extractor17.createTimer)() : (0, import_extractor17.noopTimer)();
12773
+ const timer = options.onTiming !== void 0 ? (0, import_extractor19.createTimer)() : (0, import_extractor19.noopTimer)();
11295
12774
  const cacheDir = adapterStamp.declineWhenRunFromSource(
11296
- options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? import_node_path4.default.join(options.projectRoot, ".suss", "cache") : null)
12775
+ options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? (0, import_extractor19.extractionCacheDirFor)(options.projectRoot) : null)
11297
12776
  );
11298
- const layer = (0, import_extractor17.createCacheLayer)(cacheDir);
12777
+ const layer = (0, import_extractor19.createCacheLayer)(cacheDir);
11299
12778
  const packsDigest = `${adapterStamp.packsDigest(
11300
12779
  options.packs.map(
11301
12780
  (pack) => pack.version !== void 0 ? { name: pack.name, version: pack.version } : { name: pack.name }
11302
12781
  )
11303
- )}|${(0, import_extractor17.extractionConfigStamp)({
12782
+ )}|${(0, import_extractor19.extractionConfigStamp)({
11304
12783
  gapHandling: options.gapHandling,
11305
12784
  workspaceRoot: options.workspaceRoot,
11306
12785
  projectRoot: options.projectRoot,
@@ -11308,16 +12787,19 @@ async function extractRubyProject(options) {
11308
12787
  })}`;
11309
12788
  const input = {
11310
12789
  files: cacheDir === null ? [] : options.files,
11311
- adapterPacksDigest: cacheDir === null ? packsDigest : (0, import_extractor17.runDigest)(packsDigest, options.packs, options.files)
12790
+ adapterPacksDigest: cacheDir === null ? packsDigest : (0, import_extractor19.runDigest)(packsDigest, options.packs, options.files)
11312
12791
  };
11313
12792
  const lookup = await timer.timeAsync(
11314
12793
  "cache.lookup",
11315
12794
  () => layer.lookup(input)
11316
12795
  );
12796
+ const composed = (stored) => (0, import_extractor19.composeWrappers)(stored, {
12797
+ gapHandling: options.gapHandling ?? "permissive"
12798
+ });
11317
12799
  if (lookup.kind === "hit") {
11318
12800
  options.onCacheDiagnostic?.(lookup.diagnostic);
11319
12801
  options.onTiming?.(timer.report());
11320
- return { summaries: lookup.summaries, facts: new import_datalog3.Database() };
12802
+ return { summaries: composed(lookup.summaries), facts: new import_datalog3.Database() };
11321
12803
  }
11322
12804
  const plan = lookup.diagnostic.missReason === "files-changed" ? await timer.timeAsync("cache.plan", () => layer.plan(input)) : null;
11323
12805
  if (plan !== null && plan.changed.size === 0 && plan.removed.size === 0) {
@@ -11327,7 +12809,7 @@ async function extractRubyProject(options) {
11327
12809
  }
11328
12810
  options.onCacheDiagnostic?.({ kind: "hit" });
11329
12811
  options.onTiming?.(timer.report());
11330
- return { summaries: plan.allSummaries(), facts: new import_datalog3.Database() };
12812
+ return { summaries: composed(plan.allSummaries()), facts: new import_datalog3.Database() };
11331
12813
  }
11332
12814
  const cache = { timer, cacheDir, layer, input };
11333
12815
  const records = plan === null ? null : (0, import_resolution16.recordsToReplay)(plan.roots, plan.changed, plan.removed);
@@ -11363,7 +12845,7 @@ async function runRuby(options, cache, previous) {
11363
12845
  const db = new import_datalog3.Database();
11364
12846
  const summaries = [];
11365
12847
  const gapHandling = options.gapHandling ?? "permissive";
11366
- const tallies = (0, import_extractor10.createPackTallies)(options.packs);
12848
+ const tallies = (0, import_extractor12.createPackTallies)(options.packs);
11367
12849
  const ledger = cacheDir === null ? null : new import_resolution16.DependencyLedger(options.files);
11368
12850
  const log = ledger === null ? null : new import_resolution16.FactLog(db, ledger);
11369
12851
  if (ledger !== null) {
@@ -11465,7 +12947,7 @@ async function runRuby(options, cache, previous) {
11465
12947
  continue;
11466
12948
  }
11467
12949
  discovered.add(unit.dedup);
11468
- (0, import_extractor10.tallyUnit)(tallies, unit.recognition);
12950
+ (0, import_extractor12.tallyUnit)(tallies, unit.recognition);
11469
12951
  found.push({
11470
12952
  replayed: unit.summary ?? null,
11471
12953
  seedKey: unit.seed?.key ?? null
@@ -11546,7 +13028,7 @@ async function runRuby(options, cache, previous) {
11546
13028
  continue;
11547
13029
  }
11548
13030
  discovered.add(dedup);
11549
- (0, import_extractor10.tallyUnit)(tallies, recognition);
13031
+ (0, import_extractor12.tallyUnit)(tallies, recognition);
11550
13032
  found.push({ raw, seedKey, draft: units[units.length - 1] });
11551
13033
  if (seed !== void 0 && seedKey !== null && !seedKeys.has(seedKey)) {
11552
13034
  seedKeys.add(seedKey);
@@ -11609,7 +13091,7 @@ async function runRuby(options, cache, previous) {
11609
13091
  }
11610
13092
  const summary = timer.time(
11611
13093
  "summarize",
11612
- () => (0, import_extractor17.assembleSummary)(routingGapUnit(pattern, gaps), { gapHandling })
13094
+ () => (0, import_extractor19.assembleSummary)(routingGapUnit(pattern, gaps), { gapHandling })
11613
13095
  );
11614
13096
  summary.confidence = { source: "inferred_static", level: "low" };
11615
13097
  found.push({ summary });
@@ -11665,7 +13147,7 @@ async function runRuby(options, cache, previous) {
11665
13147
  }
11666
13148
  const summary = timer.time(
11667
13149
  "summarize",
11668
- () => (0, import_extractor17.assembleSummary)(raw, { gapHandling })
13150
+ () => (0, import_extractor19.assembleSummary)(raw, { gapHandling })
11669
13151
  );
11670
13152
  summaries.push(summary);
11671
13153
  assembledHere.add(summary);
@@ -11683,13 +13165,14 @@ async function runRuby(options, cache, previous) {
11683
13165
  }
11684
13166
  if (gapHandling !== "silent") {
11685
13167
  summary.gaps.push(
11686
- ...(reached.stopsByKey.get(key) ?? []).map(import_behavioral_ir8.unfollowedCallGap)
13168
+ ...(reached.stopsByKey.get(key) ?? []).map(import_behavioral_ir9.unfollowedCallGap)
11687
13169
  );
11688
13170
  }
11689
- (0, import_behavioral_ir8.placeCalls)(summary, reached.targetsByKey.get(key));
11690
- (0, import_behavioral_ir8.placeArgTargets)(summary, reached.argTargetsByKey.get(key));
11691
- (0, import_behavioral_ir8.placeCalleeParameters)(summary, reached.parameterCallsByKey.get(key));
11692
- (0, import_behavioral_ir8.placeReceiverClasses)(summary, reached.receiverClassesByKey.get(key));
13171
+ (0, import_behavioral_ir9.placeCalls)(summary, reached.targetsByKey.get(key));
13172
+ (0, import_behavioral_ir9.placeArgTargets)(summary, reached.argTargetsByKey.get(key));
13173
+ (0, import_behavioral_ir9.placePossibleCalls)(summary, reached.possibleTargetsByKey.get(key));
13174
+ (0, import_behavioral_ir9.placeCalleeParameters)(summary, reached.parameterCallsByKey.get(key));
13175
+ (0, import_behavioral_ir9.placeReceiverClasses)(summary, reached.receiverClassesByKey.get(key));
11693
13176
  }
11694
13177
  }
11695
13178
  if (ledger !== null) {
@@ -11698,7 +13181,7 @@ async function runRuby(options, cache, previous) {
11698
13181
  }
11699
13182
  }
11700
13183
  if (gapHandling !== "silent") {
11701
- (0, import_behavioral_ir8.recordParameterGaps)(
13184
+ (0, import_behavioral_ir9.recordParameterGaps)(
11702
13185
  reached.parameterCallsByKey,
11703
13186
  summariesBySeed,
11704
13187
  reached.passedPositions
@@ -11711,20 +13194,20 @@ async function runRuby(options, cache, previous) {
11711
13194
  throw new ReplayFailed("");
11712
13195
  }
11713
13196
  const dependencies = (0, import_resolution16.importedFilesByFile)(db, displayPathOf);
11714
- (0, import_extractor17.stampModuleImports)(summaries, (file) => dependencies.get(file) ?? []);
11715
- (0, import_extractor17.stampModules)(summaries, declaredModules, options.workspaceRoot);
13197
+ (0, import_extractor19.stampModuleImports)(summaries, (file) => dependencies.get(file) ?? []);
13198
+ (0, import_extractor19.stampModules)(summaries, declaredModules, options.workspaceRoot);
11716
13199
  const idRoot = options.projectRoot ?? options.workspaceRoot;
11717
13200
  for (const summary of summaries) {
11718
13201
  const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : import_node_path4.default.resolve(options.workspaceRoot, summary.location.file);
11719
- summary.identity.id = (0, import_behavioral_ir8.summaryIdFromParts)({
13202
+ summary.identity.id = (0, import_behavioral_ir9.summaryIdFromParts)({
11720
13203
  workspace: void 0,
11721
13204
  file: idRoot === void 0 ? absoluteFile : import_node_path4.default.relative(idRoot, absoluteFile),
11722
13205
  name: summary.identity.name,
11723
13206
  exportPath: summary.identity.exportPath
11724
13207
  });
11725
13208
  }
11726
- (0, import_behavioral_ir8.disambiguateSummaryIds)(summaries);
11727
- (0, import_behavioral_ir8.linkCallsToSummaries)(summaries);
13209
+ (0, import_behavioral_ir9.disambiguateSummaryIds)(summaries);
13210
+ (0, import_behavioral_ir9.linkCallsToSummaries)(summaries);
11728
13211
  const composed = timer.time("summarize", () => {
11729
13212
  markExitCodeFunctions(
11730
13213
  summaries,
@@ -11732,18 +13215,22 @@ async function runRuby(options, cache, previous) {
11732
13215
  db,
11733
13216
  options.workspaceRoot
11734
13217
  );
11735
- return (0, import_extractor17.composeWrappers)(summaries, { gapHandling });
13218
+ return (0, import_extractor19.composeWrappers)(summaries, { gapHandling });
11736
13219
  });
13220
+ const walkedFiles = new Map(
13221
+ parsed.map(({ file }) => [displayPathOf(file), file])
13222
+ );
11737
13223
  await timer.timeAsync("cache.write", async () => {
11738
- if (cacheDir === null || composed.length === 0) {
13224
+ if (cacheDir === null || summaries.length === 0) {
11739
13225
  return;
11740
13226
  }
11741
13227
  try {
11742
13228
  await cache.layer.write(
11743
13229
  cache.input,
11744
- composed,
13230
+ summaries,
11745
13231
  log === null || ledger === null || overBudget ? void 0 : rubyAttribution({
11746
- composed,
13232
+ summaries,
13233
+ walkedFileOf: (locationFile) => walkedFiles.get(locationFile),
11747
13234
  discoveries,
11748
13235
  reached,
11749
13236
  reuse,
@@ -11818,7 +13305,10 @@ function rubyAttribution(args) {
11818
13305
  facts: args.facts.get(file) ?? { own: "", joined: "" }
11819
13306
  }
11820
13307
  }));
11821
- return { roots, owners: args.composed.map(() => []) };
13308
+ return {
13309
+ roots,
13310
+ owners: (0, import_extractor19.ownersWhereWritten)(args.summaries, args.walkedFileOf)
13311
+ };
11822
13312
  }
11823
13313
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
11824
13314
  function findRubyFiles(root) {
@@ -11892,8 +13382,8 @@ function displayNameOf(node) {
11892
13382
  if (node.type === "class" || node.type === "module") {
11893
13383
  return field(node, "name")?.text ?? node.text;
11894
13384
  }
11895
- const text2 = node.text.split("\n")[0].trim();
11896
- return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
13385
+ const text3 = node.text.split("\n")[0].trim();
13386
+ return text3.length > 40 ? `${text3.slice(0, 40)}...` : text3;
11897
13387
  }
11898
13388
  function width(node) {
11899
13389
  return node.endIndex - node.startIndex;
@@ -11923,14 +13413,14 @@ var RubyWhySession = class {
11923
13413
  * The smallest expression on `line` of `file` whose text is exactly
11924
13414
  * `text`, or null.
11925
13415
  */
11926
- findExpression(file, line, text2) {
13416
+ findExpression(file, line, text3) {
11927
13417
  const root = this.rootOf(file);
11928
13418
  if (root === null) {
11929
13419
  return null;
11930
13420
  }
11931
13421
  let found = null;
11932
13422
  const visit = (node) => {
11933
- if (node.startPosition.row + 1 === line && node.text === text2) {
13423
+ if (node.startPosition.row + 1 === line && node.text === text3) {
11934
13424
  if (found === null || width(node) <= width(found)) {
11935
13425
  found = node;
11936
13426
  }
@@ -12011,7 +13501,7 @@ var RubyWhySession = class {
12011
13501
  // src/stubEvidence.ts
12012
13502
  var import_node_fs5 = __toESM(require("fs"), 1);
12013
13503
  var import_node_path6 = __toESM(require("path"), 1);
12014
- var import_extractor18 = require("@suss/extractor");
13504
+ var import_extractor20 = require("@suss/extractor");
12015
13505
  function bareLetters(name) {
12016
13506
  return name.replaceAll(/[-_]/g, "").toLowerCase();
12017
13507
  }
@@ -12035,14 +13525,14 @@ function requireTarget(node) {
12035
13525
  }
12036
13526
  function requireSitesIn(root, file) {
12037
13527
  const found = [];
12038
- (0, import_extractor18.walkDescendants)(root, null, {
13528
+ (0, import_extractor20.walkDescendants)(root, null, children, {
12039
13529
  at: (node) => {
12040
13530
  const target = requireTarget(node);
12041
13531
  if (target !== null) {
12042
13532
  found.push({ target, file, line: rangeOf(node).start });
12043
13533
  }
12044
13534
  },
12045
- into: (node) => node.type === "argument_list" ? import_extractor18.SKIP_CHILDREN : null
13535
+ into: (node) => node.type === "argument_list" ? import_extractor20.SKIP_CHILDREN : null
12046
13536
  });
12047
13537
  return found;
12048
13538
  }
@@ -12086,6 +13576,7 @@ async function rubyStubEvidence(options) {
12086
13576
  bindEvaluator,
12087
13577
  bodyStatements,
12088
13578
  booleanLiteralValue,
13579
+ children,
12089
13580
  collectFileConstants,
12090
13581
  createConstantFileCache,
12091
13582
  createFileCache,