@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.js CHANGED
@@ -19,7 +19,29 @@ function field(node, name) {
19
19
  return node.childForFieldName(name);
20
20
  }
21
21
  function children(node) {
22
- return node.namedChildren.filter((child) => child !== null);
22
+ if (node === null || node === void 0) {
23
+ return [];
24
+ }
25
+ const found = [];
26
+ const count = node.namedChildCount;
27
+ for (let index = 0; index < count; index++) {
28
+ const child = node.namedChild(index);
29
+ if (child !== null) {
30
+ found.push(child);
31
+ }
32
+ }
33
+ return found;
34
+ }
35
+ function allChildren(node) {
36
+ const found = [];
37
+ const count = node.childCount;
38
+ for (let index = 0; index < count; index++) {
39
+ const child = node.child(index);
40
+ if (child !== null) {
41
+ found.push(child);
42
+ }
43
+ }
44
+ return found;
23
45
  }
24
46
  function bodyStatements(body) {
25
47
  return children(body);
@@ -81,6 +103,13 @@ var MODULE_SCOPE_STOPS = /* @__PURE__ */ new Set([
81
103
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
82
104
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
83
105
  var LAMBDA_TYPE = "lambda";
106
+ var RAISING_METHODS = /* @__PURE__ */ new Set(["raise", "fail"]);
107
+ function isRaiseCall(node) {
108
+ if (node.type === "identifier") {
109
+ return RAISING_METHODS.has(node.text);
110
+ }
111
+ return node.type === "call" && field(node, "receiver") === null && RAISING_METHODS.has(field(node, "method")?.text ?? "");
112
+ }
84
113
  function enclosingDefinition(node) {
85
114
  return definitionsAround.of(node);
86
115
  }
@@ -156,7 +185,7 @@ function runStatements(body, blockConfigures = () => false) {
156
185
  }
157
186
  return !(node.type === "call" && blockConfigures(node));
158
187
  };
159
- walkDescendants(body, null, {
188
+ walkDescendants(body, null, children, {
160
189
  at: (node) => {
161
190
  found.push(node);
162
191
  },
@@ -242,8 +271,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
242
271
  "protected",
243
272
  "public"
244
273
  ]);
245
- function visibilityKeyword(text2) {
246
- return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
274
+ function visibilityKeyword(text3) {
275
+ return VISIBILITY_KEYWORDS.has(text3) ? text3 : null;
247
276
  }
248
277
  function setVisibility(visibility, name, keyword) {
249
278
  if (keyword === "public") {
@@ -443,6 +472,51 @@ function readCallArgs(argumentList) {
443
472
  }
444
473
  return { positional, keyword };
445
474
  }
475
+ var CONTROL_OPERATORS = /* @__PURE__ */ new Set([
476
+ "and",
477
+ "or",
478
+ "&&",
479
+ "||"
480
+ ]);
481
+ var HANDS_ON = /* @__PURE__ */ new Set(["return", "next", "break"]);
482
+ var isChild = (fieldName) => (parent, child) => field(parent, fieldName)?.id === child.id;
483
+ var always = () => true;
484
+ var asCondition = isChild("condition");
485
+ var VALUE_USED_UNDER = {
486
+ assignment: isChild("right"),
487
+ operator_assignment: isChild("right"),
488
+ call: isChild("receiver"),
489
+ element_reference: always,
490
+ argument_list: (parent) => !HANDS_ON.has(parent.parent?.type ?? ""),
491
+ pair: always,
492
+ array: always,
493
+ hash: always,
494
+ interpolation: always,
495
+ unary: always,
496
+ binary: (parent) => !CONTROL_OPERATORS.has(field(parent, "operator")?.text ?? ""),
497
+ if: asCondition,
498
+ unless: asCondition,
499
+ while: asCondition,
500
+ until: asCondition,
501
+ if_modifier: asCondition,
502
+ unless_modifier: asCondition,
503
+ while_modifier: asCondition,
504
+ until_modifier: asCondition,
505
+ conditional: asCondition,
506
+ case: isChild("value")
507
+ };
508
+ function resultUsedAsValue(node) {
509
+ let child = node;
510
+ let parent = node.parent;
511
+ while (parent !== null && parent.type === "parenthesized_statements" && parent.namedChildCount === 1) {
512
+ child = parent;
513
+ parent = parent.parent;
514
+ }
515
+ if (parent === null) {
516
+ return false;
517
+ }
518
+ return VALUE_USED_UNDER[parent.type]?.(parent, child) ?? false;
519
+ }
446
520
  var NodeSet = class extends IdSet {
447
521
  };
448
522
  var NodeMap = class extends IdMap {
@@ -468,8 +542,8 @@ function lowerAcronyms(word, acronyms) {
468
542
  (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
469
543
  );
470
544
  }
471
- function escapeRegExp(text2) {
472
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
545
+ function escapeRegExp(text3) {
546
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
473
547
  }
474
548
  var PATH_CONVENTIONS = {
475
549
  railsUnderscore: underscoreConstantPath
@@ -477,19 +551,23 @@ var PATH_CONVENTIONS = {
477
551
  function createConstantFileCache() {
478
552
  return { autoloadDirectories: /* @__PURE__ */ new Map(), resolved: /* @__PURE__ */ new Map() };
479
553
  }
480
- function resolveConstantFile(root, qualifiedName, convention, acronyms = [], cache = createConstantFileCache()) {
554
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = [], cache = createConstantFileCache(), autoloadRoots = []) {
481
555
  const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
482
- const key = `${root}\0${relative}`;
556
+ const key = [root, ...autoloadRoots, relative].join("\0");
483
557
  const known = cache.resolved.get(key);
484
558
  if (known !== void 0) {
485
559
  return known;
486
560
  }
487
- const found = firstExisting(root, relative, cache);
561
+ const found = firstExisting(root, relative, cache, autoloadRoots);
488
562
  cache.resolved.set(key, found);
489
563
  return found;
490
564
  }
491
- function firstExisting(root, relative, cache) {
492
- for (const candidate of [root, ...autoloadDirectoriesOf(root, cache)]) {
565
+ function firstExisting(root, relative, cache, autoloadRoots) {
566
+ for (const candidate of [
567
+ root,
568
+ ...autoloadDirectoriesOf(root, cache),
569
+ ...autoloadRoots
570
+ ]) {
493
571
  const file = path.join(candidate, relative);
494
572
  if (fs.existsSync(file)) {
495
573
  return file;
@@ -528,7 +606,7 @@ function isDirectory(directory) {
528
606
  }
529
607
 
530
608
  // src/defineMethod.ts
531
- import { nodeOfKey as nodeOfKey2 } from "@suss/resolution";
609
+ import { nodeOfKey as nodeOfKey2, spanOfKey } from "@suss/resolution";
532
610
  import { hole as hole2, literalOf as literalOf3, piecesOf } from "@suss/values";
533
611
 
534
612
  // src/values/evaluator.ts
@@ -536,10 +614,12 @@ import { nodeOfKey, noteKeyRead, writtenValuesByKey } from "@suss/resolution";
536
614
  import { Evaluator, force, literalOf, text } from "@suss/values";
537
615
 
538
616
  // src/facts/resolve.ts
617
+ import { isTestFile } from "@suss/behavioral-ir";
539
618
  import { constant, lit, rule, variable as v } from "@suss/datalog";
540
619
  import {
541
620
  allocationSitesOf,
542
621
  alsoSteps,
622
+ answersFromTestsLeftOut,
543
623
  askResolution,
544
624
  askResolutionUnder,
545
625
  askSources,
@@ -565,6 +645,80 @@ var CONVERTING_FUNCTIONS = /* @__PURE__ */ new Set([
565
645
  "String",
566
646
  "Float"
567
647
  ]);
648
+ var OBJECT_METHODS = /* @__PURE__ */ new Set([
649
+ "Array",
650
+ "Float",
651
+ "Hash",
652
+ "Integer",
653
+ "String",
654
+ "abort",
655
+ "binding",
656
+ "block_given?",
657
+ "caller",
658
+ "catch",
659
+ "class",
660
+ "clone",
661
+ "define_singleton_method",
662
+ "dup",
663
+ "eql?",
664
+ "equal?",
665
+ "eval",
666
+ "exit",
667
+ "extend",
668
+ "fail",
669
+ "format",
670
+ "freeze",
671
+ "frozen?",
672
+ "hash",
673
+ "inspect",
674
+ "instance_of?",
675
+ "instance_variable_defined?",
676
+ "instance_variable_get",
677
+ "instance_variable_set",
678
+ "instance_variables",
679
+ "is_a?",
680
+ "itself",
681
+ "kind_of?",
682
+ "lambda",
683
+ "loop",
684
+ "method",
685
+ "methods",
686
+ "nil?",
687
+ "object_id",
688
+ "p",
689
+ "pp",
690
+ "print",
691
+ "printf",
692
+ "proc",
693
+ "public_send",
694
+ "puts",
695
+ "raise",
696
+ "rand",
697
+ "require",
698
+ "require_relative",
699
+ "respond_to?",
700
+ "send",
701
+ "singleton_class",
702
+ "sleep",
703
+ "sprintf",
704
+ "srand",
705
+ "system",
706
+ "tap",
707
+ "then",
708
+ "throw",
709
+ "to_s",
710
+ "warn",
711
+ "yield_self"
712
+ ]);
713
+ var SENDING_METHODS = /* @__PURE__ */ new Set([
714
+ "send",
715
+ "public_send",
716
+ "__send__"
717
+ ]);
718
+ var METHOD_REFERENCE = "method";
719
+ var INVOKE_METHOD = "call";
720
+ var BLOCK_WRAPPERS = /* @__PURE__ */ new Set(["proc", "lambda"]);
721
+ var PROC_CLASS = "Proc";
568
722
  var CONSTRUCTOR = "new";
569
723
 
570
724
  // src/facts/resolve.ts
@@ -612,7 +766,13 @@ function resolveEnvObjects(db, objects) {
612
766
  }
613
767
  function writtenValueOf(db, key) {
614
768
  resolveValues(db, [key]);
615
- return sharedWrittenValueOf(db, key, (keys) => resolveValues(db, keys));
769
+ return sharedWrittenValueOf(
770
+ db,
771
+ key,
772
+ (keys) => resolveValues(db, keys),
773
+ void 0,
774
+ answersFromTestsLeftOut(key, isTestFile)
775
+ );
616
776
  }
617
777
  function writtenValueUnder(db, key, site) {
618
778
  const program = resolutionUnderProgram(RUBY_RULES);
@@ -630,12 +790,16 @@ function constructionSites(db, classKey) {
630
790
 
631
791
  // src/facts/values.ts
632
792
  import {
793
+ addEntryRows,
633
794
  addPackWords,
795
+ addTemplateRows,
634
796
  classMemberName,
635
797
  NAMED_STORE_NAME,
636
798
  RECEIVER_STORE_NAME,
799
+ SPREAD_ENTRY,
637
800
  startsAtName,
638
801
  UNPLACED_STORE_NAME,
802
+ VALUE_ENTRY,
639
803
  valueLeftByWrites
640
804
  } from "@suss/resolution";
641
805
 
@@ -652,7 +816,7 @@ function isFieldOf(parent, node, name) {
652
816
  function namedAt(parent, node) {
653
817
  return isFieldOf(parent, node, "name");
654
818
  }
655
- function always() {
819
+ function always2() {
656
820
  return true;
657
821
  }
658
822
  var SPELLS_A_NAME = {
@@ -667,14 +831,14 @@ var SPELLS_A_NAME = {
667
831
  hash_splat_parameter: namedAt,
668
832
  keyword_parameter: namedAt,
669
833
  block_parameter: namedAt,
670
- destructured_parameter: always,
671
- left_assignment_list: always,
672
- rest_assignment: always,
673
- destructured_left_assignment: always,
674
- exception_variable: always,
675
- undef: always,
676
- alias: always,
677
- ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always]))
834
+ destructured_parameter: always2,
835
+ left_assignment_list: always2,
836
+ rest_assignment: always2,
837
+ destructured_left_assignment: always2,
838
+ exception_variable: always2,
839
+ undef: always2,
840
+ alias: always2,
841
+ ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always2]))
678
842
  };
679
843
  function addNamesUnder(node, names) {
680
844
  if (node === null) {
@@ -1168,6 +1332,9 @@ function add(emitter, relation, ...tuple) {
1168
1332
  }
1169
1333
  emitter.db.add(relation, tuple);
1170
1334
  }
1335
+ function rowsOf(emitter) {
1336
+ return (relation, row) => add(emitter, relation, ...row);
1337
+ }
1171
1338
  function valueKey(emitter, written, parent) {
1172
1339
  return readKey(emitter.filePath, written, emitter.enclosing, parent);
1173
1340
  }
@@ -1251,6 +1418,97 @@ function emitCall(emitter, site) {
1251
1418
  }
1252
1419
  emitCallArguments(emitter, site);
1253
1420
  emitConversion(emitter, site);
1421
+ emitSentChoice(emitter, site);
1422
+ emitMethodReference(emitter, site);
1423
+ emitBlockAsProc(emitter, site);
1424
+ }
1425
+ function emitBlockAsProc(emitter, site) {
1426
+ const { method, receiver, node } = site;
1427
+ const name = method?.text;
1428
+ const wraps = receiver === null ? name !== void 0 && BLOCK_WRAPPERS.has(name) : name === CONSTRUCTOR && receiver.text === PROC_CLASS;
1429
+ const block = field(node, "block");
1430
+ if (!wraps || block === null) {
1431
+ return;
1432
+ }
1433
+ const blockKey = nodeId(emitter.filePath, block);
1434
+ add(emitter, "func", blockKey);
1435
+ add(emitter, "binds", site.key, blockKey);
1436
+ for (const call of callsWrittenIn(block)) {
1437
+ add(emitter, "makesCall", blockKey, nodeId(emitter.filePath, call));
1438
+ }
1439
+ }
1440
+ function callsWrittenIn(node, found = []) {
1441
+ for (const child of children(node)) {
1442
+ if (OWN_BODY_TYPES.has(child.type)) {
1443
+ continue;
1444
+ }
1445
+ if (child.type === "call") {
1446
+ found.push(child);
1447
+ }
1448
+ callsWrittenIn(child, found);
1449
+ }
1450
+ return found;
1451
+ }
1452
+ function emitSentChoice(emitter, site) {
1453
+ const { method, receiver, node } = site;
1454
+ if (method === null || !SENDING_METHODS.has(method.text)) {
1455
+ return;
1456
+ }
1457
+ const objectKey = receiver === null ? emitter.selfKey : valueKey(emitter, receiver, node);
1458
+ const [name] = readCallArgs(field(node, "arguments")).positional;
1459
+ if (objectKey === null || name === void 0) {
1460
+ return;
1461
+ }
1462
+ add(emitter, "callsChosenEntry", site.key);
1463
+ const written = symbolValue(name) ?? stringLiteralValue(name);
1464
+ if (written !== null) {
1465
+ for (const spelled of namesReadAs(emitter, receiver, objectKey, written)) {
1466
+ add(emitter, "choosesNamedEntry", site.key, objectKey, spelled);
1467
+ }
1468
+ return;
1469
+ }
1470
+ const ends = writtenEnds(name);
1471
+ if (ends === null) {
1472
+ add(emitter, "choosesEntry", site.key, objectKey);
1473
+ return;
1474
+ }
1475
+ for (const start of namesReadAs(emitter, receiver, objectKey, ends.start)) {
1476
+ add(emitter, "choosesEntryLike", site.key, objectKey, start, ends.end);
1477
+ }
1478
+ }
1479
+ function writtenEnds(node) {
1480
+ if (node.type !== "string" && node.type !== "delimited_symbol") {
1481
+ return null;
1482
+ }
1483
+ const parts = children(node);
1484
+ const written = (part) => part.type === "string_content";
1485
+ const firstComputed = parts.findIndex((part) => !written(part));
1486
+ if (firstComputed === -1) {
1487
+ return null;
1488
+ }
1489
+ const lastComputed = parts.length - 1 - [...parts].reverse().findIndex((part) => !written(part));
1490
+ const start = parts.slice(0, firstComputed).map((part) => part.text).join("");
1491
+ const end = parts.slice(lastComputed + 1).map((part) => part.text).join("");
1492
+ return start === "" && end === "" ? null : { start, end };
1493
+ }
1494
+ function emitMethodReference(emitter, site) {
1495
+ const { method, receiver, node } = site;
1496
+ if (method?.text !== METHOD_REFERENCE || receiver !== null || emitter.selfKey === null) {
1497
+ return;
1498
+ }
1499
+ const [name] = readCallArgs(field(node, "arguments")).positional;
1500
+ const written = name === void 0 ? null : symbolValue(name) ?? stringLiteralValue(name);
1501
+ if (written === null) {
1502
+ return;
1503
+ }
1504
+ for (const spelled of namesReadAs(
1505
+ emitter,
1506
+ receiver,
1507
+ emitter.selfKey,
1508
+ written
1509
+ )) {
1510
+ add(emitter, "readsProperty", site.key, emitter.selfKey, spelled);
1511
+ }
1254
1512
  }
1255
1513
  function emitConversion(emitter, site) {
1256
1514
  const { method, receiver, node } = site;
@@ -1324,7 +1582,6 @@ var SELF_READS = {
1324
1582
  function classSpelling(name) {
1325
1583
  return name === CONSTRUCTOR ? name : classMemberName(name);
1326
1584
  }
1327
- var INVOKE_METHOD = "call";
1328
1585
  function invokedValueOf(call) {
1329
1586
  if (call.type !== "call") {
1330
1587
  return null;
@@ -1370,6 +1627,7 @@ function emitArray(emitter, array) {
1370
1627
  const objectKey = nodeId(emitter.filePath, array);
1371
1628
  add(emitter, "objectValue", objectKey);
1372
1629
  let position = 0;
1630
+ const entries = [];
1373
1631
  for (const element of children(array)) {
1374
1632
  add(
1375
1633
  emitter,
@@ -1379,12 +1637,24 @@ function emitArray(emitter, array) {
1379
1637
  valueKey(emitter, element, array)
1380
1638
  );
1381
1639
  position += 1;
1640
+ entries.push(writtenEntry(emitter, element, array));
1382
1641
  }
1642
+ addEntryRows(rowsOf(emitter), objectKey, entries);
1643
+ }
1644
+ var SPREAD_TYPES = /* @__PURE__ */ new Set(["splat_argument", "hash_splat_argument"]);
1645
+ function writtenEntry(emitter, entry, parent) {
1646
+ const unpacked = SPREAD_TYPES.has(entry.type) ? children(entry)[0] : void 0;
1647
+ return unpacked === void 0 ? { key: valueKey(emitter, entry, parent), form: VALUE_ENTRY } : { key: valueKey(emitter, unpacked, entry), form: SPREAD_ENTRY };
1383
1648
  }
1384
1649
  function emitHash(emitter, hash) {
1385
1650
  const objectKey = nodeId(emitter.filePath, hash);
1386
1651
  add(emitter, "objectValue", objectKey);
1652
+ const entries = [];
1387
1653
  for (const pair of children(hash)) {
1654
+ if (SPREAD_TYPES.has(pair.type)) {
1655
+ entries.push(writtenEntry(emitter, pair, hash));
1656
+ continue;
1657
+ }
1388
1658
  if (pair.type !== "pair") {
1389
1659
  continue;
1390
1660
  }
@@ -1393,6 +1663,7 @@ function emitHash(emitter, hash) {
1393
1663
  if (key === null || value === null) {
1394
1664
  continue;
1395
1665
  }
1666
+ entries.push(writtenEntry(emitter, value, pair));
1396
1667
  const keyText = pairKeyText(key);
1397
1668
  if (keyText === null) {
1398
1669
  add(emitter, "holdsUnderKey", objectKey, valueKey(emitter, value, pair));
@@ -1406,32 +1677,66 @@ function emitHash(emitter, hash) {
1406
1677
  valueKey(emitter, value, pair)
1407
1678
  );
1408
1679
  }
1680
+ addEntryRows(rowsOf(emitter), objectKey, entries);
1409
1681
  }
1410
1682
  var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1411
1683
  var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
1412
- var DEFINE_METHOD_CALL = "define_method";
1413
- function loopTurnAt(emitter, block, call) {
1414
- if (call.type !== "call") {
1415
- return null;
1416
- }
1417
- const receiver = field(call, "receiver");
1418
- const method = field(call, "method");
1684
+ var COUNTING_LOOP = "each_with_index";
1685
+ function emitLoopVariables(emitter, block, call) {
1686
+ const receiver = call.type === "call" ? field(call, "receiver") : null;
1687
+ const method = receiver === null ? null : field(call, "method");
1419
1688
  if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
1420
- return null;
1689
+ return;
1421
1690
  }
1422
- const parameters = field(block, "parameters");
1423
- const names = parameters === null ? [] : children(parameters).map((p) => p.text);
1424
- const element = names[0];
1425
- if (element === void 0) {
1426
- return null;
1691
+ const variables = children(field(block, "parameters")).filter(
1692
+ (variable) => variable.type !== "block_parameter"
1693
+ );
1694
+ const counting = method.text === COUNTING_LOOP;
1695
+ const [first, position] = variables;
1696
+ if (first === void 0 || counting && variables.length > 2) {
1697
+ return;
1427
1698
  }
1428
- return {
1429
- element,
1430
- index: names[1] ?? "",
1431
- overKey: nodeId(emitter.filePath, receiver)
1432
- };
1699
+ const taken = counting || variables.length === 1 ? elementTakenBy(first) : partsTakenBy(variables);
1700
+ const blockKey = nodeId(emitter.filePath, block);
1701
+ const over = valueKey(emitter, receiver, call);
1702
+ for (const variable of taken) {
1703
+ const key = `${blockKey}#${variable.name}`;
1704
+ if (variable.part === null) {
1705
+ add(emitter, "elementOf", key, over);
1706
+ continue;
1707
+ }
1708
+ add(emitter, "elementPartOf", key, over, variable.part);
1709
+ }
1710
+ const [anchor] = taken;
1711
+ if (counting && position?.type === "identifier" && anchor !== void 0) {
1712
+ add(
1713
+ emitter,
1714
+ "positionOf",
1715
+ `${blockKey}#${position.text}`,
1716
+ `${blockKey}#${anchor.name}`
1717
+ );
1718
+ }
1719
+ }
1720
+ function elementTakenBy(variable) {
1721
+ if (variable.type === "identifier") {
1722
+ return [{ name: variable.text, part: null }];
1723
+ }
1724
+ return variable.type === "destructured_parameter" ? partsTakenBy(children(variable)) : [];
1725
+ }
1726
+ function partsTakenBy(variables) {
1727
+ const parts = [];
1728
+ for (const [position, variable] of variables.entries()) {
1729
+ if (variable.type === "splat_parameter") {
1730
+ break;
1731
+ }
1732
+ if (variable.type === "identifier") {
1733
+ parts.push({ name: variable.text, part: String(position) });
1734
+ }
1735
+ }
1736
+ return parts;
1433
1737
  }
1434
- function emitDynamicDefinition(emitter, call, type, turns) {
1738
+ var DEFINE_METHOD_CALL = "define_method";
1739
+ function emitDynamicDefinition(emitter, call, type) {
1435
1740
  if (emitter.selfKey === null || // A call inside a method runs when the method runs, not when the class loads.
1436
1741
  emitter.enclosing !== null || type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
1437
1742
  return;
@@ -1440,33 +1745,16 @@ function emitDynamicDefinition(emitter, call, type, turns) {
1440
1745
  const first = args === null ? void 0 : children(args)[0];
1441
1746
  const nameKey2 = nodeId(emitter.filePath, first ?? call);
1442
1747
  add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
1443
- for (const turn of turns) {
1444
- add(
1445
- emitter,
1446
- "nameTurnsOn",
1447
- nameKey2,
1448
- turn.element,
1449
- turn.index,
1450
- turn.overKey
1451
- );
1452
- }
1453
1748
  }
1454
- function walkExpressions(node, emitter, visit, nested = null, turns = []) {
1749
+ function walkExpressions(node, visit, nested = null) {
1455
1750
  for (const child of children(node)) {
1456
1751
  const type = child.type;
1457
1752
  if (OWN_BODY_TYPES.has(type)) {
1458
1753
  nested?.push(child);
1459
1754
  continue;
1460
1755
  }
1461
- visit(child, type, node, turns);
1462
- const opened = BLOCK_TYPES3.has(type) ? loopTurnAt(emitter, child, node) : null;
1463
- walkExpressions(
1464
- child,
1465
- emitter,
1466
- visit,
1467
- nested,
1468
- opened === null ? turns : [...turns, opened]
1469
- );
1756
+ visit(child, type, node);
1757
+ walkExpressions(child, visit, nested);
1470
1758
  }
1471
1759
  }
1472
1760
  var WRITTEN_KEY_TYPES = /* @__PURE__ */ new Set([
@@ -1480,22 +1768,23 @@ function emitKeyedRead(emitter, site, container, key, keyParent) {
1480
1768
  if (WRITTEN_KEY_TYPES.has(key.type)) {
1481
1769
  return;
1482
1770
  }
1771
+ const siteKey = nodeId(emitter.filePath, site);
1772
+ const containerKey = valueKey(emitter, container, site);
1483
1773
  add(
1484
1774
  emitter,
1485
1775
  "readsKeyed",
1486
- nodeId(emitter.filePath, site),
1487
- valueKey(emitter, container, site),
1776
+ siteKey,
1777
+ containerKey,
1488
1778
  valueKey(emitter, key, keyParent)
1489
1779
  );
1780
+ add(emitter, "choosesEntry", siteKey, containerKey);
1490
1781
  }
1491
1782
  function emitKeyedElement(emitter, node, parent) {
1492
1783
  const object = field(node, "object");
1493
1784
  if (object === null || isWriteTarget(node, parent)) {
1494
1785
  return;
1495
1786
  }
1496
- const index = node.namedChildren.find(
1497
- (child) => child !== null && child.id !== object.id
1498
- );
1787
+ const index = children(node).find((child) => child.id !== object.id);
1499
1788
  if (index !== void 0) {
1500
1789
  emitKeyedRead(emitter, node, object, index, node);
1501
1790
  emitWrittenKeyRead(emitter, node, object, index);
@@ -1536,27 +1825,27 @@ function fallbackBranchesOf(node, type, enclosing) {
1536
1825
  }
1537
1826
  return [left, right].filter((side) => !raises(side, enclosing));
1538
1827
  }
1539
- var RAISING_METHODS = /* @__PURE__ */ new Set(["raise", "fail"]);
1828
+ var RAISING_METHODS2 = /* @__PURE__ */ new Set(["raise", "fail"]);
1540
1829
  function raises(written, enclosing) {
1541
1830
  const node = readThrough(written);
1542
1831
  if (node.type === "identifier") {
1543
- return RAISING_METHODS.has(node.text) && isBareCall(node, enclosing);
1832
+ return RAISING_METHODS2.has(node.text) && isBareCall(node, enclosing);
1544
1833
  }
1545
1834
  if (node.type !== "call") {
1546
1835
  return false;
1547
1836
  }
1548
1837
  const receiver = field(node, "receiver");
1549
- return RAISING_METHODS.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1838
+ return RAISING_METHODS2.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1550
1839
  }
1551
1840
  function emitExpressionFacts(emitter, node) {
1552
- emitDynamicDefinition(emitter, node, node.type, []);
1553
- walkExpressions(node, emitter, (child, type, parent, turns) => {
1841
+ emitDynamicDefinition(emitter, node, node.type);
1842
+ walkExpressions(node, (child, type, parent) => {
1554
1843
  const site = callSiteAt(emitter, child, type, parent);
1555
- emitExpressionFact(emitter, child, type, parent, turns, site);
1844
+ emitExpressionFact(emitter, child, type, parent, site);
1556
1845
  });
1557
1846
  }
1558
- function emitExpressionFact(emitter, child, type, parent, turns, site) {
1559
- emitDynamicDefinition(emitter, child, type, turns);
1847
+ function emitExpressionFact(emitter, child, type, parent, site) {
1848
+ emitDynamicDefinition(emitter, child, type);
1560
1849
  if (site !== null) {
1561
1850
  emitCall(emitter, site);
1562
1851
  emitKeyedFetch(emitter, site);
@@ -1597,6 +1886,11 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1597
1886
  }
1598
1887
  if (BLOCK_TYPES3.has(type)) {
1599
1888
  emitBlockValue(emitter, child);
1889
+ emitLoopVariables(emitter, child, parent);
1890
+ }
1891
+ const pieces = TEMPLATE_PIECES[type]?.(emitter, child) ?? [];
1892
+ if (pieces.length > 0) {
1893
+ addTemplateRows(rowsOf(emitter), nodeId(emitter.filePath, child), pieces);
1600
1894
  }
1601
1895
  for (const branch of BRANCHES_OF[type]?.(child) ?? []) {
1602
1896
  add(
@@ -1616,6 +1910,29 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1616
1910
  emitParameterDefault(emitter, child);
1617
1911
  }
1618
1912
  }
1913
+ function interpolatedPieces(emitter, string) {
1914
+ const parts = children(string);
1915
+ if (!parts.some((part) => part.type === "interpolation")) {
1916
+ return [];
1917
+ }
1918
+ return parts.flatMap((part) => {
1919
+ if (part.type === "string_content") {
1920
+ return [{ text: part.text }];
1921
+ }
1922
+ const expression = part.type === "interpolation" ? children(part)[0] : void 0;
1923
+ return expression === void 0 ? [] : [{ hole: valueKey(emitter, expression, part) }];
1924
+ });
1925
+ }
1926
+ function adjacentPieces(emitter, joined2) {
1927
+ return children(joined2).map((part) => ({
1928
+ hole: valueKey(emitter, part, joined2)
1929
+ }));
1930
+ }
1931
+ var TEMPLATE_PIECES = {
1932
+ string: interpolatedPieces,
1933
+ delimited_symbol: interpolatedPieces,
1934
+ chained_string: adjacentPieces
1935
+ };
1619
1936
  function emitBlockArgument(emitter, argument) {
1620
1937
  const [handed] = children(argument);
1621
1938
  if (handed !== void 0) {
@@ -2038,11 +2355,10 @@ function emitMethodFacts(emitter, method) {
2038
2355
  const collecting = { ...inside, heldRows: expressionFacts };
2039
2356
  walkExpressions(
2040
2357
  body,
2041
- inside,
2042
- (child, type, parent, turns) => {
2358
+ (child, type, parent) => {
2043
2359
  const site = callSiteAt(inside, child, type, parent);
2044
2360
  emitBodyFact(bodyFacts, inside, funcKey, child, type, site);
2045
- emitExpressionFact(collecting, child, type, parent, turns, site);
2361
+ emitExpressionFact(collecting, child, type, parent, site);
2046
2362
  },
2047
2363
  nested
2048
2364
  );
@@ -2502,9 +2818,7 @@ var EXPRESSION_TYPES = {
2502
2818
  ),
2503
2819
  unary: (node) => {
2504
2820
  const operand2 = field(node, "operand");
2505
- const operator = node.children.find(
2506
- (child) => child !== null && !child.isNamed
2507
- );
2821
+ const operator = allChildren(node).find((child) => !child.isNamed);
2508
2822
  if (operand2 === null || operator === void 0) {
2509
2823
  return OPAQUE;
2510
2824
  }
@@ -2706,8 +3020,25 @@ function statementOf(node) {
2706
3020
  if (appending !== null) {
2707
3021
  return appending;
2708
3022
  }
3023
+ if (isRaiseCall(node) && !rescuedInSameFunction(node)) {
3024
+ return { kind: "throw" };
3025
+ }
2709
3026
  return isTailExpression(node) ? { kind: "return", value: node } : { kind: "expression", value: node };
2710
3027
  }
3028
+ var RESCUING_TYPES = /* @__PURE__ */ new Set(["begin", "body_statement"]);
3029
+ var UNRESCUED_PARTS = /* @__PURE__ */ new Set(["rescue", "else", "ensure"]);
3030
+ function rescuedInSameFunction(node) {
3031
+ let child = node;
3032
+ let parent = node.parent;
3033
+ while (parent !== null && !isRoot(parent)) {
3034
+ if (RESCUING_TYPES.has(parent.type) && !UNRESCUED_PARTS.has(child.type) && children(parent).some((part) => part.type === "rescue")) {
3035
+ return true;
3036
+ }
3037
+ child = parent;
3038
+ parent = parent.parent;
3039
+ }
3040
+ return false;
3041
+ }
2711
3042
  function appendingStatement(node) {
2712
3043
  if (node.type !== "binary" || field(node, "operator")?.text !== "<<") {
2713
3044
  return null;
@@ -3382,6 +3713,10 @@ function writtenNodeOf(node, db, site) {
3382
3713
  const context = db === void 0 ? void 0 : contexts.get(db);
3383
3714
  return context === void 0 ? null : context.writtenTo(node, site);
3384
3715
  }
3716
+ function calledDefinition(call, db) {
3717
+ const context = db === void 0 ? void 0 : contexts.get(db);
3718
+ return context === void 0 ? null : context.callable(call);
3719
+ }
3385
3720
  function askWrittenValues(nodes, db) {
3386
3721
  if (db === void 0 || !contexts.has(db)) {
3387
3722
  return;
@@ -3444,7 +3779,7 @@ function methodDefinitionsIn(file, root) {
3444
3779
  if (METHOD_TYPES.has(node.type)) {
3445
3780
  found.set(nodeId(file, node), node);
3446
3781
  }
3447
- for (const child of node.namedChildren) {
3782
+ for (const child of children(node)) {
3448
3783
  if (child !== null) {
3449
3784
  visit(child);
3450
3785
  }
@@ -3507,7 +3842,7 @@ function contextOver(db, nodes) {
3507
3842
  }
3508
3843
 
3509
3844
  // src/values/literals.ts
3510
- import { force as force2, literalOf as literalOf2 } from "@suss/values";
3845
+ import { force as force2, literalOf as literalOf2, sequence, text as text2 } from "@suss/values";
3511
3846
  function literalElementsIn(value) {
3512
3847
  if (value.kind !== "sequence") {
3513
3848
  return null;
@@ -3522,8 +3857,24 @@ function literalElementsIn(value) {
3522
3857
  }
3523
3858
  return elements;
3524
3859
  }
3525
- function literalElementsOf(node, db) {
3526
- return literalElementsIn(evaluatedValue(node, db));
3860
+ function loopElementsOf(node, db) {
3861
+ const value = evaluatedValue(node, db);
3862
+ if (value.kind === "sequence") {
3863
+ return value.items.map((item) => elementOfList(force2(item.value)));
3864
+ }
3865
+ if (value.kind === "record" && !value.open) {
3866
+ return [...value.fields].map(([key, item]) => {
3867
+ const parts = [text2(key), force2(item.value)];
3868
+ return { whole: sequence(parts), parts };
3869
+ });
3870
+ }
3871
+ return null;
3872
+ }
3873
+ function elementOfList(whole) {
3874
+ return {
3875
+ whole,
3876
+ parts: whole.kind === "sequence" ? whole.items.map((item) => force2(item.value)) : [whole]
3877
+ };
3527
3878
  }
3528
3879
  function namesOf(node, db) {
3529
3880
  const value = evaluatedValue(node, db);
@@ -3545,13 +3896,16 @@ function definedNamesOf(dynamic, classKey) {
3545
3896
  }
3546
3897
  function readDynamicNames(db, rootsByFile) {
3547
3898
  const byClass = /* @__PURE__ */ new Map();
3548
- for (const [classKey, nameKey2] of db.facts("definesMethodFrom")) {
3899
+ const definitions = db.facts("definesMethodFrom");
3900
+ const loops = definitions.length === 0 ? /* @__PURE__ */ new Map() : loopsByFile(db);
3901
+ for (const [classKey, nameKey2] of definitions) {
3549
3902
  const key = String(classKey);
3903
+ const around = loopsAround(loops, key, String(nameKey2));
3550
3904
  byClass.set(
3551
3905
  key,
3552
3906
  foldReading(
3553
3907
  byClass.get(key) ?? NOTHING,
3554
- readOne(db, rootsByFile, String(nameKey2))
3908
+ readOne(db, rootsByFile, String(nameKey2), around)
3555
3909
  )
3556
3910
  );
3557
3911
  }
@@ -3572,11 +3926,11 @@ function foldReading(soFar, reading) {
3572
3926
  unreadable: soFar.unreadable
3573
3927
  };
3574
3928
  }
3575
- function readOne(db, rootsByFile, nameKey2) {
3929
+ function readOne(db, rootsByFile, nameKey2, loops) {
3576
3930
  const node = nodeOfKey2(rootsByFile, nameKey2);
3577
3931
  const names = [];
3578
3932
  const patterns = [];
3579
- for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
3933
+ for (const bindings of loopTurns(db, rootsByFile, loops)) {
3580
3934
  const value = node === null ? hole2(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
3581
3935
  const name = literalOf3(value);
3582
3936
  if (name !== null) {
@@ -3609,38 +3963,105 @@ function patternOf(value) {
3609
3963
  }
3610
3964
  return readSomething ? new RegExp(`${source}$`) : null;
3611
3965
  }
3612
- function escapeForPattern(text2) {
3613
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3966
+ function escapeForPattern(text3) {
3967
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3614
3968
  }
3615
- function loopTurns(db, rootsByFile, nameKey2) {
3969
+ function loopTurns(db, rootsByFile, loops) {
3616
3970
  let turns = [/* @__PURE__ */ new Map()];
3617
- for (const over of listsBehind(db, rootsByFile, nameKey2)) {
3971
+ for (const loop of loops) {
3972
+ const bindings = bindingsOf(db, rootsByFile, loop);
3973
+ if (bindings === null) {
3974
+ continue;
3975
+ }
3618
3976
  turns = turns.flatMap(
3619
- (base) => over.elements.map((value, position) => {
3620
- const binding = new Map([...base, [over.element, value]]);
3621
- if (over.index !== "") {
3622
- binding.set(over.index, String(position));
3623
- }
3624
- return binding;
3625
- })
3977
+ (base) => bindings.map((binding) => new Map([...base, ...binding]))
3626
3978
  );
3627
3979
  }
3628
3980
  return turns;
3629
3981
  }
3630
- function listsBehind(db, rootsByFile, nameKey2) {
3631
- const found = [];
3632
- for (const [, element, index, overKey] of db.lookup(
3633
- "nameTurnsOn",
3634
- 0,
3635
- nameKey2
3636
- )) {
3637
- const node = nodeOfKey2(rootsByFile, String(overKey));
3638
- const elements = node === null ? null : literalElementsOf(node, db);
3639
- if (elements !== null) {
3640
- found.push({ element: String(element), index: String(index), elements });
3982
+ function bindingsOf(db, rootsByFile, loop) {
3983
+ const node = collectionNode(rootsByFile, loop);
3984
+ const elements = node === null ? null : loopElementsOf(node, db);
3985
+ if (elements === null) {
3986
+ return null;
3987
+ }
3988
+ const bound = [...loop.variables].flatMap(([name, takes]) => {
3989
+ const values = elements.map(
3990
+ (element, position) => GIVEN[takes.kind](element, position, takes)
3991
+ );
3992
+ return values.every((value) => value !== null) ? [{ name, values }] : [];
3993
+ });
3994
+ if (bound.length === 0) {
3995
+ return null;
3996
+ }
3997
+ return elements.map(
3998
+ (_, position) => new Map(bound.map(({ name, values }) => [name, values[position] ?? ""]))
3999
+ );
4000
+ }
4001
+ var GIVEN = {
4002
+ element: (element) => literalOf3(element.whole),
4003
+ part: (element, _, takes) => {
4004
+ const part = takes.kind === "part" ? element.parts[takes.part] : void 0;
4005
+ return part === void 0 ? null : literalOf3(part);
4006
+ },
4007
+ position: (_, position) => String(position)
4008
+ };
4009
+ function collectionNode(rootsByFile, loop) {
4010
+ const written = nodeOfKey2(rootsByFile, loop.over);
4011
+ if (written !== null) {
4012
+ return written;
4013
+ }
4014
+ const call = nodeOfKey2(rootsByFile, loop.block.key)?.parent ?? null;
4015
+ return call === null ? null : field(call, "receiver");
4016
+ }
4017
+ function blockVariableOf(key) {
4018
+ const at = key.lastIndexOf("#");
4019
+ const span = at < 0 ? null : spanOfKey(key.slice(0, at));
4020
+ return span === null ? null : { block: { ...span, key: key.slice(0, at) }, name: key.slice(at + 1) };
4021
+ }
4022
+ function loopsByFile(db) {
4023
+ const blocks = /* @__PURE__ */ new Map();
4024
+ const note = (key, over, takes) => {
4025
+ const variable = blockVariableOf(String(key));
4026
+ if (variable === null) {
4027
+ return;
3641
4028
  }
4029
+ const known = blocks.get(variable.block.key);
4030
+ const loop = known ?? {
4031
+ block: variable.block,
4032
+ over: String(over),
4033
+ variables: /* @__PURE__ */ new Map()
4034
+ };
4035
+ loop.variables.set(variable.name, takes);
4036
+ blocks.set(variable.block.key, loop);
4037
+ };
4038
+ for (const [element, over] of db.facts("elementOf")) {
4039
+ note(element, over, { kind: "element" });
3642
4040
  }
3643
- return found;
4041
+ for (const [element, over, part] of db.facts("elementPartOf")) {
4042
+ note(element, over, { kind: "part", part: Number(part) });
4043
+ }
4044
+ for (const [position, element] of db.facts("positionOf")) {
4045
+ const block = blockVariableOf(String(element))?.block.key;
4046
+ if (block !== void 0 && blocks.has(block)) {
4047
+ note(position, blocks.get(block)?.over, { kind: "position" });
4048
+ }
4049
+ }
4050
+ const byFile = /* @__PURE__ */ new Map();
4051
+ for (const loop of blocks.values()) {
4052
+ byFile.set(loop.block.file, [...byFile.get(loop.block.file) ?? [], loop]);
4053
+ }
4054
+ return byFile;
4055
+ }
4056
+ function loopsAround(loops, classKey, nameKey2) {
4057
+ const cls = spanOfKey(classKey);
4058
+ const name = spanOfKey(nameKey2);
4059
+ if (cls === null || name === null) {
4060
+ return [];
4061
+ }
4062
+ return (loops.get(name.file) ?? []).filter(
4063
+ ({ block }) => cls.start <= block.start && block.end <= cls.end && block.start <= name.start && name.end <= block.end
4064
+ ).sort((a, b) => a.block.start - b.block.start);
3644
4065
  }
3645
4066
 
3646
4067
  // src/scope.ts
@@ -3770,12 +4191,27 @@ function graphqlTypeNameFromQualified(qualifiedName, convention) {
3770
4191
 
3771
4192
  // src/ancestry.ts
3772
4193
  async function reachDefinition(qualifiedName, lookup) {
4194
+ const segments = qualifiedName.split("::");
4195
+ for (let length = segments.length; length > 0; length--) {
4196
+ const found = await definedInFileOf(
4197
+ segments.slice(0, length).join("::"),
4198
+ qualifiedName,
4199
+ lookup
4200
+ );
4201
+ if (found !== null) {
4202
+ return found;
4203
+ }
4204
+ }
4205
+ return null;
4206
+ }
4207
+ async function definedInFileOf(fileConstant, qualifiedName, lookup) {
3773
4208
  const filePath = resolveConstantFile(
3774
4209
  lookup.root,
3775
- qualifiedName,
4210
+ fileConstant,
3776
4211
  lookup.pathConvention,
3777
4212
  lookup.acronyms,
3778
- lookup.constantFiles
4213
+ lookup.constantFiles,
4214
+ lookup.autoloadRoots
3779
4215
  );
3780
4216
  if (filePath === null) {
3781
4217
  return null;
@@ -3920,7 +4356,7 @@ function superclassCandidatesOf(blocks) {
3920
4356
  return [];
3921
4357
  }
3922
4358
  function methodInAncestry(ancestry, name, read = {}) {
3923
- for (const entry of ancestry) {
4359
+ for (const [index, entry] of ancestry.entries()) {
3924
4360
  if (entry.type === "root") {
3925
4361
  return { type: "none" };
3926
4362
  }
@@ -3928,7 +4364,8 @@ function methodInAncestry(ancestry, name, read = {}) {
3928
4364
  return {
3929
4365
  type: "unsettled",
3930
4366
  reason: `inherited from ${entry.name}, which this run did not read`,
3931
- cause: "unreadAncestor"
4367
+ cause: "unreadAncestor",
4368
+ definedLater: definedInProjectAfter(ancestry, index, name, read)
3932
4369
  };
3933
4370
  }
3934
4371
  const found = definitionIn(entry.blocks, name, read);
@@ -3945,6 +4382,18 @@ function methodInAncestry(ancestry, name, read = {}) {
3945
4382
  }
3946
4383
  return { type: "none" };
3947
4384
  }
4385
+ function definedInProjectAfter(ancestry, index, name, read) {
4386
+ return ancestry.slice(index + 1).some(
4387
+ (entry) => entry.type !== "root" && entry.type !== "unfollowed" && definitionIn(entry.blocks, name, read).method !== null
4388
+ );
4389
+ }
4390
+ function methodPastUnreadAncestors(ancestry, name, read = {}) {
4391
+ return methodInAncestry(
4392
+ ancestry.filter((entry) => entry.type !== "unfollowed"),
4393
+ name,
4394
+ read
4395
+ );
4396
+ }
3948
4397
  function definitionIn(blocks, name, read) {
3949
4398
  let method = null;
3950
4399
  let block = null;
@@ -3994,11 +4443,17 @@ import {
3994
4443
  graphqlResolverBinding,
3995
4444
  restBinding as restBinding2
3996
4445
  } from "@suss/behavioral-ir";
3997
- import { absentReading as absentReading2, unreadableReading as unreadableReading2 } from "@suss/extractor";
4446
+ import { absentReading as absentReading3, unreadableReading as unreadableReading2 } from "@suss/extractor";
3998
4447
 
3999
4448
  // src/clientCalls.ts
4000
- import { hasNameHole, namesNothing, restBinding } from "@suss/behavioral-ir";
4001
- import { pathOf } from "@suss/values";
4449
+ import {
4450
+ hasNameHole,
4451
+ namesNothing,
4452
+ restBinding,
4453
+ statusMembersOf,
4454
+ testsStatus
4455
+ } from "@suss/behavioral-ir";
4456
+ import { hostOf, pathOf } from "@suss/values";
4002
4457
 
4003
4458
  // src/paths/effects.ts
4004
4459
  import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
@@ -4025,7 +4480,7 @@ function isRaise(node) {
4025
4480
  return false;
4026
4481
  }
4027
4482
  const receiver = field(node, "receiver");
4028
- const method = field(node, "method") ?? node.namedChildren[0] ?? null;
4483
+ const method = field(node, "method") ?? node.namedChild(0) ?? null;
4029
4484
  return receiver === null && method !== null && RAISE_NAMES.has(method.text);
4030
4485
  }
4031
4486
  function exitOf(node) {
@@ -4037,7 +4492,7 @@ function exitOf(node) {
4037
4492
  function exitKindOf(node) {
4038
4493
  let sawReturn = false;
4039
4494
  const raises2 = (current) => {
4040
- for (const child of current.namedChildren) {
4495
+ for (const child of children(current)) {
4041
4496
  if (child === null || CAPTURES_RETURN.has(child.type)) {
4042
4497
  continue;
4043
4498
  }
@@ -4071,13 +4526,52 @@ function callHeaderOf(call, block) {
4071
4526
  function blockStatements(block) {
4072
4527
  return block === null ? [] : children(block);
4073
4528
  }
4529
+ function attachedBlockOf(call) {
4530
+ return children(call).find(
4531
+ (child) => child !== null && (child.type === "do_block" || child.type === "block")
4532
+ ) ?? null;
4533
+ }
4534
+ function soleBlockParameter(block) {
4535
+ const names = children(field(block, "parameters")).filter(
4536
+ (child) => child.type === "identifier"
4537
+ );
4538
+ return names.length === 1 ? names[0].text : null;
4539
+ }
4540
+ function blockBodyStatements(block) {
4541
+ const body = field(block, "body");
4542
+ if (body !== null) {
4543
+ return children(body);
4544
+ }
4545
+ return children(block).filter((child) => child.type !== "block_parameters");
4546
+ }
4547
+ function armBody(block) {
4548
+ return field(block, "body") ?? block;
4549
+ }
4550
+ function formatArmOf(statement, parameter) {
4551
+ if (statement.type !== "call") {
4552
+ return null;
4553
+ }
4554
+ const receiver = field(statement, "receiver");
4555
+ const format = field(statement, "method")?.text;
4556
+ if (receiver?.text !== parameter || format === void 0) {
4557
+ return null;
4558
+ }
4559
+ const block = attachedBlockOf(statement);
4560
+ return {
4561
+ format,
4562
+ header: block === null ? statement.text : callHeaderOf(statement, block).sourceText,
4563
+ block
4564
+ };
4565
+ }
4074
4566
  var Lowerer = class {
4075
- constructor(terminals, responseCalls) {
4567
+ constructor(terminals, responseCalls, formatSwitch) {
4076
4568
  this.terminals = terminals;
4077
4569
  this.responseCalls = responseCalls;
4570
+ this.formatSwitch = formatSwitch;
4078
4571
  }
4079
4572
  terminals;
4080
4573
  responseCalls;
4574
+ formatSwitch;
4081
4575
  terminalsByStmt = /* @__PURE__ */ new Map();
4082
4576
  terminalHome = new NodeMap();
4083
4577
  attachTerminals(statement, found) {
@@ -4108,7 +4602,7 @@ var Lowerer = class {
4108
4602
  found.push(own);
4109
4603
  return found;
4110
4604
  }
4111
- for (const child of node.namedChildren) {
4605
+ for (const child of children(node)) {
4112
4606
  if (child !== null && !OWN_BODY_TYPES.has(child.type)) {
4113
4607
  this.responsesIn(child, found);
4114
4608
  }
@@ -4167,29 +4661,22 @@ var Lowerer = class {
4167
4661
  };
4168
4662
  }
4169
4663
  lowerBegin(node) {
4170
- const rescues = node.namedChildren.filter(
4171
- (child) => child !== null && child.type === "rescue"
4172
- );
4173
- const ensureClause = node.namedChildren.find(
4174
- (child) => child !== null && child.type === "ensure"
4175
- ) ?? null;
4176
- const tryBody = node.namedChildren.filter(
4664
+ const rescues2 = children(node).filter((child) => child.type === "rescue");
4665
+ const ensureClause = children(node).find((child) => child.type === "ensure") ?? null;
4666
+ const tryBody = children(node).filter(
4177
4667
  (child) => child !== null && child.type !== "rescue" && child.type !== "ensure" && child.type !== "else"
4178
4668
  );
4179
4669
  return {
4180
4670
  kind: "try",
4181
4671
  tryBody: tryBody.map((statement) => this.lower(statement)),
4182
- catchBody: rescues.length > 0 ? rescues.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4672
+ catchBody: rescues2.length > 0 ? rescues2.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4183
4673
  finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
4184
4674
  exitKind: exitKindOf(node)
4185
4675
  };
4186
4676
  }
4187
4677
  lowerCase(node) {
4188
4678
  const groups = [];
4189
- for (const child of node.namedChildren) {
4190
- if (child === null) {
4191
- continue;
4192
- }
4679
+ for (const child of children(node)) {
4193
4680
  if (child.type === "when") {
4194
4681
  groups.push({
4195
4682
  condition: handleOf(field(child, "pattern")),
@@ -4207,6 +4694,47 @@ var Lowerer = class {
4207
4694
  }
4208
4695
  return { kind: "switch", groups, exitKind: exitKindOf(node) };
4209
4696
  }
4697
+ /**
4698
+ * `respond_to do |format| ... end` runs one arm per request, picked by
4699
+ * the format the request accepts, so it lowers to a switch with an arm
4700
+ * per `format.x`. The arm that accepts every format is the default.
4701
+ * Without one, a request no arm accepts makes the library raise. Null
4702
+ * when the block contains anything besides those arms.
4703
+ */
4704
+ lowerFormatSwitch(node, block) {
4705
+ const declared = this.formatSwitch;
4706
+ if (declared === void 0 || node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== declared.name) {
4707
+ return null;
4708
+ }
4709
+ const parameter = soleBlockParameter(block);
4710
+ if (parameter === null) {
4711
+ return null;
4712
+ }
4713
+ const arms = blockBodyStatements(block).map(
4714
+ (statement) => formatArmOf(statement, parameter)
4715
+ );
4716
+ if (arms.length === 0 || arms.some((arm) => arm === null)) {
4717
+ return null;
4718
+ }
4719
+ const groups = arms.map(
4720
+ (arm) => ({
4721
+ condition: arm.format === declared.matchesEvery ? null : { sourceText: arm.header, expression: null },
4722
+ hasTrailingBreak: true,
4723
+ body: arm.block === null ? [] : this.lowerBlock(armBody(arm.block))
4724
+ })
4725
+ );
4726
+ const named2 = groups.filter((group) => group.condition !== null);
4727
+ const fallback2 = groups.find((group) => group.condition === null) ?? {
4728
+ condition: null,
4729
+ hasTrailingBreak: true,
4730
+ body: [{ kind: "exit", exit: "throw", exitKind: "throw" }]
4731
+ };
4732
+ return {
4733
+ kind: "switch",
4734
+ groups: [...named2, fallback2],
4735
+ exitKind: exitKindOf(node)
4736
+ };
4737
+ }
4210
4738
  lower(node) {
4211
4739
  const exit = exitOf(node);
4212
4740
  if (exit !== null) {
@@ -4236,10 +4764,12 @@ var Lowerer = class {
4236
4764
  [header]
4237
4765
  );
4238
4766
  }
4239
- const attachedBlock = node.namedChildren.find(
4240
- (child) => child !== null && (child.type === "do_block" || child.type === "block")
4241
- );
4242
- if (node.type === "call" && attachedBlock !== void 0) {
4767
+ const attachedBlock = attachedBlockOf(node);
4768
+ const formatSwitch = attachedBlock === null ? null : this.lowerFormatSwitch(node, attachedBlock);
4769
+ if (formatSwitch !== null) {
4770
+ return this.attach(formatSwitch, [node]);
4771
+ }
4772
+ if (node.type === "call" && attachedBlock !== null) {
4243
4773
  return this.attach(
4244
4774
  {
4245
4775
  kind: "loop",
@@ -4266,10 +4796,11 @@ var Lowerer = class {
4266
4796
  return this.attach({ kind: "opaque", exitKind: exitKindOf(node) }, [node]);
4267
4797
  }
4268
4798
  };
4269
- function lowerRubyBody(body, terminals, responseCalls = []) {
4799
+ function lowerRubyBody(body, terminals, responseCalls = [], formatSwitch) {
4270
4800
  const lowerer = new Lowerer(
4271
4801
  new Map(terminals.map((terminal) => [terminal.id, terminal])),
4272
- new NodeSet(responseCalls)
4802
+ new NodeSet(responseCalls),
4803
+ formatSwitch
4273
4804
  );
4274
4805
  return {
4275
4806
  statements: lowerer.lowerBlock(body),
@@ -4289,9 +4820,9 @@ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
4289
4820
  ]);
4290
4821
  function literalArgOf(node, facts) {
4291
4822
  const value = evaluatedValue(node, facts);
4292
- const text2 = literalOf4(value);
4293
- if (text2 !== null) {
4294
- return { kind: "string", value: text2 };
4823
+ const text3 = literalOf4(value);
4824
+ if (text3 !== null) {
4825
+ return { kind: "string", value: text3 };
4295
4826
  }
4296
4827
  const constant2 = constantOf(value);
4297
4828
  if (typeof constant2 === "number") {
@@ -4357,7 +4888,7 @@ function isCall(node, locals, inherited) {
4357
4888
  }
4358
4889
  function collectCalls(node, read, inherited, found) {
4359
4890
  for (const child of children(node)) {
4360
- if (read.stops.has(child.type)) {
4891
+ if (read.stops.has(child.type) && !isLambdaArgument(child)) {
4361
4892
  continue;
4362
4893
  }
4363
4894
  if (isCall(child, read.locals, inherited)) {
@@ -4367,6 +4898,9 @@ function collectCalls(node, read, inherited, found) {
4367
4898
  }
4368
4899
  return found;
4369
4900
  }
4901
+ function isLambdaArgument(node) {
4902
+ return node.type === LAMBDA_TYPE && node.parent?.type === "argument_list";
4903
+ }
4370
4904
  function methodBody(definitionNode) {
4371
4905
  const body = field(definitionNode, "body");
4372
4906
  if (body === null) {
@@ -4430,6 +4964,14 @@ function enclosingStatement(call, body) {
4430
4964
  return current;
4431
4965
  }
4432
4966
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4967
+ return invocationSites(
4968
+ definitionNode,
4969
+ inherited,
4970
+ keepsArglessCall,
4971
+ facts
4972
+ ).map((site) => site.effect);
4973
+ }
4974
+ function invocationSites(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4433
4975
  const read = methodBody(definitionNode);
4434
4976
  if (read === null) {
4435
4977
  return [];
@@ -4442,7 +4984,7 @@ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, ke
4442
4984
  inherited,
4443
4985
  keepsArglessCall,
4444
4986
  facts
4445
- );
4987
+ ).map((site) => site.effect);
4446
4988
  }
4447
4989
  function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4448
4990
  const body = read.body;
@@ -4476,25 +5018,18 @@ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4476
5018
  statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
4477
5019
  );
4478
5020
  return {
4479
- type: "invocation",
4480
- callee: calleeText(call),
4481
- args: argsOf(call, facts),
4482
- async: false,
4483
- ...conditions.length > 0 ? { preconditions: conditions } : {}
5021
+ call,
5022
+ effect: {
5023
+ type: "invocation",
5024
+ callee: calleeText(call),
5025
+ args: argsOf(call, facts),
5026
+ async: false,
5027
+ ...conditions.length > 0 ? { preconditions: conditions } : {}
5028
+ }
4484
5029
  };
4485
5030
  });
4486
5031
  }
4487
5032
 
4488
- // src/responseStatus.ts
4489
- import {
4490
- absentReading,
4491
- enumerateOrDegrade as enumerateOrDegrade2,
4492
- guardsHoldOn,
4493
- unreadableReading,
4494
- writtenReading
4495
- } from "@suss/extractor";
4496
- import { constantOf as constantOf3, literalOf as literalOf7 } from "@suss/values";
4497
-
4498
5033
  // src/paths/predicates.ts
4499
5034
  var OPERATORS = {
4500
5035
  "==": "eq",
@@ -4513,11 +5048,11 @@ var opaqueOf = (node) => ({
4513
5048
  var JOINING = /* @__PURE__ */ new Set(["&&", "||", "and", "or"]);
4514
5049
  function predicateOf(node, inputs) {
4515
5050
  if (node.type === "parenthesized_statements") {
4516
- const inner = node.namedChildren[0];
5051
+ const inner = node.namedChild(0);
4517
5052
  return inner == null ? opaqueOf(node) : predicateOf(inner, inputs);
4518
5053
  }
4519
5054
  if (node.type === "unary" && field(node, "operator")?.text === "!") {
4520
- const operand2 = field(node, "operand") ?? node.namedChildren[0];
5055
+ const operand2 = field(node, "operand") ?? node.namedChild(0);
4521
5056
  return operand2 == null ? opaqueOf(node) : { type: "negation", operand: predicateOf(operand2, inputs) };
4522
5057
  }
4523
5058
  if (node.type === "binary") {
@@ -4543,11 +5078,11 @@ function predicateOf(node, inputs) {
4543
5078
  }
4544
5079
  function operandsOf(node) {
4545
5080
  if (node.type === "parenthesized_statements") {
4546
- const inner = node.namedChildren[0];
5081
+ const inner = node.namedChild(0);
4547
5082
  return inner == null ? [] : operandsOf(inner);
4548
5083
  }
4549
5084
  if (node.type === "unary" && field(node, "operator")?.text === "!") {
4550
- const operand2 = field(node, "operand") ?? node.namedChildren[0];
5085
+ const operand2 = field(node, "operand") ?? node.namedChild(0);
4551
5086
  return operand2 == null ? [] : operandsOf(operand2);
4552
5087
  }
4553
5088
  if (node.type === "binary") {
@@ -4643,6 +5178,321 @@ function calledMethod(node) {
4643
5178
  return field(node, "method")?.text ?? null;
4644
5179
  }
4645
5180
 
5181
+ // src/responseHandOff.ts
5182
+ import { helperPathsOnVariables, splitAtHandOffs } from "@suss/extractor";
5183
+
5184
+ // src/exceptionClasses.ts
5185
+ var ROOT_EXCEPTION = "Exception";
5186
+ var RUBY_EXCEPTION_PARENTS = {
5187
+ NoMemoryError: ROOT_EXCEPTION,
5188
+ ScriptError: ROOT_EXCEPTION,
5189
+ LoadError: "ScriptError",
5190
+ NotImplementedError: "ScriptError",
5191
+ SyntaxError: "ScriptError",
5192
+ SecurityError: ROOT_EXCEPTION,
5193
+ SignalException: ROOT_EXCEPTION,
5194
+ Interrupt: "SignalException",
5195
+ StandardError: ROOT_EXCEPTION,
5196
+ SystemExit: ROOT_EXCEPTION,
5197
+ SystemStackError: ROOT_EXCEPTION,
5198
+ ArgumentError: "StandardError",
5199
+ UncaughtThrowError: "ArgumentError",
5200
+ EncodingError: "StandardError",
5201
+ FiberError: "StandardError",
5202
+ IOError: "StandardError",
5203
+ EOFError: "IOError",
5204
+ IndexError: "StandardError",
5205
+ KeyError: "IndexError",
5206
+ StopIteration: "IndexError",
5207
+ ClosedQueueError: "StopIteration",
5208
+ LocalJumpError: "StandardError",
5209
+ NameError: "StandardError",
5210
+ NoMethodError: "NameError",
5211
+ RangeError: "StandardError",
5212
+ FloatDomainError: "RangeError",
5213
+ RegexpError: "StandardError",
5214
+ RuntimeError: "StandardError",
5215
+ FrozenError: "RuntimeError",
5216
+ SystemCallError: "StandardError",
5217
+ ThreadError: "StandardError",
5218
+ TypeError: "StandardError",
5219
+ ZeroDivisionError: "StandardError"
5220
+ };
5221
+ var MESSAGE_ONLY_EXCEPTION = "RuntimeError";
5222
+ var ExceptionReader = class {
5223
+ constructor(lookup, library) {
5224
+ this.library = library;
5225
+ this.walking = {
5226
+ ...lookup,
5227
+ ancestryRootClassNames: [
5228
+ ...Object.keys(library),
5229
+ ...Object.keys(RUBY_EXCEPTION_PARENTS),
5230
+ ROOT_EXCEPTION
5231
+ ]
5232
+ };
5233
+ }
5234
+ library;
5235
+ walking;
5236
+ known = /* @__PURE__ */ new Map();
5237
+ read(ref) {
5238
+ const key = [ref.text, ...ref.candidates].join("\0");
5239
+ let found = this.known.get(key);
5240
+ if (found === void 0) {
5241
+ found = readException(ref, this.walking, this.library);
5242
+ this.known.set(key, found);
5243
+ }
5244
+ return found;
5245
+ }
5246
+ };
5247
+ function listedException(name, library) {
5248
+ const fromLibrary = library[name];
5249
+ if (fromLibrary !== void 0) {
5250
+ return {
5251
+ name,
5252
+ ancestors: fromLibrary.ancestors,
5253
+ incomplete: false,
5254
+ inheritableByUnread: false
5255
+ };
5256
+ }
5257
+ const fromRuby = rubyAncestors(name);
5258
+ return fromRuby === null ? null : {
5259
+ name,
5260
+ ancestors: fromRuby,
5261
+ incomplete: false,
5262
+ inheritableByUnread: true
5263
+ };
5264
+ }
5265
+ function unreadException(name) {
5266
+ return {
5267
+ name,
5268
+ ancestors: [ROOT_EXCEPTION],
5269
+ incomplete: true,
5270
+ inheritableByUnread: true
5271
+ };
5272
+ }
5273
+ function classesOf(exception) {
5274
+ return [exception.name, ...exception.ancestors];
5275
+ }
5276
+ async function readException(ref, lookup, library) {
5277
+ for (const candidate of ref.candidates) {
5278
+ const known = listedException(candidate, library);
5279
+ if (known !== null) {
5280
+ return known;
5281
+ }
5282
+ const defined = await reachConstant([candidate], lookup);
5283
+ if (defined !== null) {
5284
+ return projectException(defined, lookup, library);
5285
+ }
5286
+ }
5287
+ return unreadException(ref.candidates.at(-1) ?? ref.text);
5288
+ }
5289
+ async function projectException(defined, lookup, library) {
5290
+ const chain = await ancestryOf(defined.name, defined.blocks, lookup);
5291
+ const ancestors = [];
5292
+ let incomplete = false;
5293
+ for (const entry of chain) {
5294
+ if (entry.type === "bodies" && entry.name === defined.name) {
5295
+ continue;
5296
+ }
5297
+ ancestors.push(entry.name);
5298
+ if (entry.type === "root") {
5299
+ ancestors.push(
5300
+ ...listedException(entry.name, library)?.ancestors ?? []
5301
+ );
5302
+ }
5303
+ if (entry.type === "unfollowed") {
5304
+ incomplete = true;
5305
+ }
5306
+ }
5307
+ if (incomplete && !ancestors.includes(ROOT_EXCEPTION)) {
5308
+ ancestors.push(ROOT_EXCEPTION);
5309
+ }
5310
+ return {
5311
+ name: defined.name,
5312
+ ancestors,
5313
+ incomplete,
5314
+ inheritableByUnread: false
5315
+ };
5316
+ }
5317
+ function rubyAncestors(name) {
5318
+ if (name === ROOT_EXCEPTION) {
5319
+ return [];
5320
+ }
5321
+ if (RUBY_EXCEPTION_PARENTS[name] === void 0) {
5322
+ return null;
5323
+ }
5324
+ const ancestors = [];
5325
+ for (let parent = RUBY_EXCEPTION_PARENTS[name]; parent !== void 0; parent = RUBY_EXCEPTION_PARENTS[parent]) {
5326
+ ancestors.push(parent);
5327
+ }
5328
+ return ancestors;
5329
+ }
5330
+
5331
+ // src/raises.ts
5332
+ var RESCUING_BODIES = /* @__PURE__ */ new Set(["begin", "body_statement"]);
5333
+ var AFTER_CATCH = /* @__PURE__ */ new Set(["rescue", "else", "ensure"]);
5334
+ function escapingRaises(method) {
5335
+ const body = field(method, "body");
5336
+ if (body === null) {
5337
+ return [];
5338
+ }
5339
+ const found = [];
5340
+ const visit = (node) => {
5341
+ for (const child of children(node)) {
5342
+ if (child === null || OWN_BODY_TYPES.has(child.type)) {
5343
+ continue;
5344
+ }
5345
+ if (isRaise(child) && !caughtLocally(child, method)) {
5346
+ found.push(child);
5347
+ continue;
5348
+ }
5349
+ visit(child);
5350
+ }
5351
+ };
5352
+ visit(body);
5353
+ return found;
5354
+ }
5355
+ function caughtLocally(raise, method) {
5356
+ let child = raise;
5357
+ for (let parent = raise.parent; parent !== null && parent.id !== method.id; parent = parent.parent) {
5358
+ if (rescues(parent, child)) {
5359
+ return true;
5360
+ }
5361
+ child = parent;
5362
+ }
5363
+ return false;
5364
+ }
5365
+ function rescues(parent, child) {
5366
+ if (parent.type === "rescue_modifier") {
5367
+ return field(parent, "body")?.id === child.id;
5368
+ }
5369
+ return RESCUING_BODIES.has(parent.type) && !AFTER_CATCH.has(child.type) && children(parent).some((one) => one?.type === "rescue");
5370
+ }
5371
+ function raisedClassRef(call, nesting) {
5372
+ const first = readCallArgs(field(call, "arguments")).positional[0];
5373
+ if (first === void 0) {
5374
+ return null;
5375
+ }
5376
+ if (first.type === "string") {
5377
+ return {
5378
+ text: MESSAGE_ONLY_EXCEPTION,
5379
+ candidates: [MESSAGE_ONLY_EXCEPTION]
5380
+ };
5381
+ }
5382
+ const constant2 = constructedClass(first) ?? first;
5383
+ const candidates = constantRefCandidates(constant2, nesting);
5384
+ return candidates.length === 0 ? null : { text: constant2.text, candidates };
5385
+ }
5386
+ function constructedClass(node) {
5387
+ if (node.type !== "call" || field(node, "method")?.text !== "new") {
5388
+ return null;
5389
+ }
5390
+ return field(node, "receiver");
5391
+ }
5392
+
5393
+ // src/responseHandOff.ts
5394
+ function raiseEndings(method) {
5395
+ return escapingRaises(method).map((call) => ({
5396
+ call,
5397
+ terminal: raiseTerminal(call)
5398
+ }));
5399
+ }
5400
+ function raiseTerminal(call) {
5401
+ const [first, second] = readCallArgs(field(call, "arguments")).positional;
5402
+ const message = first === void 0 ? null : stringLiteralValue(first);
5403
+ return {
5404
+ kind: "throw",
5405
+ statusCode: null,
5406
+ body: null,
5407
+ exceptionType: raisedClassRef(call, [])?.text ?? null,
5408
+ message: message ?? (second === void 0 ? null : stringLiteralValue(second)),
5409
+ component: null,
5410
+ renderTree: null,
5411
+ delegateTarget: null,
5412
+ emitEvent: null,
5413
+ location: rangeOf(call)
5414
+ };
5415
+ }
5416
+ function throughResponseHelpers(branches, sites, options) {
5417
+ const handOffs = [];
5418
+ for (const { call, effect } of sites) {
5419
+ const helper = helperBranchesAt(call, options);
5420
+ if (helper !== null) {
5421
+ handOffs.push({
5422
+ helper,
5423
+ preconditions: effect.preconditions,
5424
+ line: rangeOf(call).start,
5425
+ at: rangeOf(call)
5426
+ });
5427
+ }
5428
+ }
5429
+ return handOffs.length === 0 ? [...branches] : branches.flatMap((branch) => splitAtHandOffs(branch, handOffs));
5430
+ }
5431
+ function helperBranchesAt(call, options) {
5432
+ if (call.type !== "call") {
5433
+ return null;
5434
+ }
5435
+ const args = readCallArgs(field(call, "arguments"));
5436
+ const handedAt = args.positional.flatMap(
5437
+ (argument, position) => argument.type === "identifier" && refersToResponse(argument, options) ? [{ position, variable: argument.text }] : []
5438
+ );
5439
+ const handedAs = Object.entries(args.keyword).flatMap(
5440
+ ([name, argument]) => argument.type === "identifier" && refersToResponse(argument, options) ? [{ name, variable: argument.text }] : []
5441
+ );
5442
+ if (handedAt.length === 0 && handedAs.length === 0) {
5443
+ return null;
5444
+ }
5445
+ const helper = calledDefinition(call, options.facts);
5446
+ if (helper === null) {
5447
+ return null;
5448
+ }
5449
+ const passed = /* @__PURE__ */ new Map();
5450
+ const names = parametersOf(helper).map(
5451
+ (parameter) => paramNameOf(parameter)?.text ?? null
5452
+ );
5453
+ for (const { position, variable } of handedAt) {
5454
+ const name = names[position];
5455
+ if (name !== void 0 && name !== null) {
5456
+ passed.set(name, variable);
5457
+ }
5458
+ }
5459
+ for (const { name, variable } of handedAs) {
5460
+ if (names.includes(name)) {
5461
+ passed.set(name, variable);
5462
+ }
5463
+ }
5464
+ return passed.size === 0 ? null : helperPathsOnVariables(options.readHelper(helper), passed);
5465
+ }
5466
+ function refersToResponse(name, options) {
5467
+ const written = writtenNodeOf(name, options.facts);
5468
+ if (written === null) {
5469
+ return false;
5470
+ }
5471
+ if (sameNode(written, options.request)) {
5472
+ return true;
5473
+ }
5474
+ const member = written.type === "call" ? field(written, "method") : null;
5475
+ const receiver = written.type === "call" ? field(written, "receiver") : null;
5476
+ if (member === null || receiver?.type !== "identifier" || !options.bodyMethods.includes(member.text)) {
5477
+ return false;
5478
+ }
5479
+ const parsedFrom = writtenNodeOf(receiver, options.facts);
5480
+ return parsedFrom !== null && sameNode(parsedFrom, options.request);
5481
+ }
5482
+ function sameNode(a, b) {
5483
+ return a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.tree === b.tree;
5484
+ }
5485
+
5486
+ // src/responseStatus.ts
5487
+ import {
5488
+ absentReading as absentReading2,
5489
+ enumerateOrDegrade as enumerateOrDegrade2,
5490
+ guardsHoldOn,
5491
+ unreadableReading,
5492
+ writtenReading as writtenReading2
5493
+ } from "@suss/extractor";
5494
+ import { constantOf as constantOf3, literalOf as literalOf7 } from "@suss/values";
5495
+
4646
5496
  // src/provenance.ts
4647
5497
  import { sourceRefsOf } from "@suss/extractor";
4648
5498
  import { constantOf as constantOf2, literalOf as literalOf6 } from "@suss/values";
@@ -4710,7 +5560,7 @@ function pickedSource(call, loader) {
4710
5560
  return null;
4711
5561
  }
4712
5562
  const args = field(pick, "arguments");
4713
- const given = args === null ? void 0 : args.namedChildren[source.at];
5563
+ const given = args === null ? void 0 : args.namedChild(source.at);
4714
5564
  return given?.type === "constant" || given?.type === "scope_resolution" ? given : null;
4715
5565
  }
4716
5566
 
@@ -5355,10 +6205,7 @@ function recognizeMethodStorage(method, file, options) {
5355
6205
  return { effects, slots };
5356
6206
  }
5357
6207
  function callsUnder(node, found = []) {
5358
- for (const child of node.namedChildren) {
5359
- if (child === null) {
5360
- continue;
5361
- }
6208
+ for (const child of children(node)) {
5362
6209
  if (child.type === "call") {
5363
6210
  found.push(child);
5364
6211
  }
@@ -5566,6 +6413,111 @@ function literalAt(key, facts) {
5566
6413
  return constant2 !== void 0 ? constant2 : literalOf6(value) ?? void 0;
5567
6414
  }
5568
6415
 
6416
+ // src/raisedStatuses.ts
6417
+ function rescuedBy(handlers, exception) {
6418
+ if (handlers === "eachRoute") {
6419
+ return true;
6420
+ }
6421
+ const classes = new Set(classesOf(exception));
6422
+ return handlers.some(
6423
+ (caught) => caught.someUnread || caught.classes.some(
6424
+ (one) => classes.has(one.name) || exception.incomplete && one.inheritableByUnread
6425
+ )
6426
+ );
6427
+ }
6428
+ function raisedTerminal(pattern, exception, handlers, location) {
6429
+ const libraryStatus = exception === null ? void 0 : pattern.libraryExceptions?.[exception.name]?.status;
6430
+ const status = exception === null || rescuedBy(handlers, exception) ? void 0 : libraryStatus;
6431
+ return {
6432
+ kind: status === void 0 ? "throw" : "response",
6433
+ statusCode: status === void 0 ? null : { type: "literal", value: status },
6434
+ body: null,
6435
+ exceptionType: status === void 0 ? exception?.name ?? null : null,
6436
+ ...status === void 0 && exception !== null ? {
6437
+ exceptionAncestry: {
6438
+ ancestors: exception.ancestors,
6439
+ incomplete: exception.incomplete
6440
+ }
6441
+ } : {},
6442
+ ...status === void 0 && libraryStatus !== void 0 ? { statusWhenUncaught: libraryStatus } : {},
6443
+ message: null,
6444
+ component: null,
6445
+ renderTree: null,
6446
+ delegateTarget: null,
6447
+ emitEvent: null,
6448
+ location
6449
+ };
6450
+ }
6451
+ function raisedStatusBranches(pattern, storage, effects, handlers, location) {
6452
+ const operations = new Set(
6453
+ (effects ?? []).flatMap(
6454
+ (effect) => effect.type === "interaction" && effect.interaction.class === "storage-access" && effect.interaction.operation !== void 0 ? [effect.interaction.operation] : []
6455
+ )
6456
+ );
6457
+ const raised = new Set(
6458
+ storage.flatMap(
6459
+ (one) => (one.raises ?? []).flatMap(
6460
+ (raise) => raise.methods.some((method) => operations.has(method)) ? [raise.exception] : []
6461
+ )
6462
+ )
6463
+ );
6464
+ return [...raised].flatMap((name) => {
6465
+ const exception = listedException(name, pattern.libraryExceptions ?? {}) ?? unreadException(name);
6466
+ const terminal = raisedTerminal(pattern, exception, handlers, location);
6467
+ if (terminal.kind === "throw" && !rescuedBy(handlers, exception)) {
6468
+ return [];
6469
+ }
6470
+ return [
6471
+ {
6472
+ conditions: [
6473
+ {
6474
+ sourceText: `${name} raised`,
6475
+ structured: null,
6476
+ polarity: "positive",
6477
+ source: "earlyThrow"
6478
+ }
6479
+ ],
6480
+ terminal,
6481
+ effects: [],
6482
+ location,
6483
+ isDefault: false
6484
+ }
6485
+ ];
6486
+ });
6487
+ }
6488
+
6489
+ // src/responseBody.ts
6490
+ import { absentReading, writtenReading } from "@suss/extractor";
6491
+ import { shapeOfValue } from "@suss/values";
6492
+ var BODY_KINDS = /* @__PURE__ */ new Set([
6493
+ "record",
6494
+ "sequence",
6495
+ "unbounded"
6496
+ ]);
6497
+ function serializedValue(node, serializers) {
6498
+ const receiver = field(node, "receiver");
6499
+ const method = field(node, "method")?.text;
6500
+ if (node.type !== "call" || receiver === null || method === void 0 || !serializers.has(method) || field(node, "arguments") !== null) {
6501
+ return node;
6502
+ }
6503
+ return serializedValue(receiver, serializers);
6504
+ }
6505
+ function bodyReadingOfCall(call, declaration, facts) {
6506
+ if (declaration.bodyKeyword === void 0 || call.type !== "call") {
6507
+ return absentReading;
6508
+ }
6509
+ const argument = readCallArgs(field(call, "arguments")).keyword[declaration.bodyKeyword];
6510
+ if (argument === void 0) {
6511
+ return absentReading;
6512
+ }
6513
+ const body = serializedValue(
6514
+ argument,
6515
+ new Set(declaration.bodySerializers ?? [])
6516
+ );
6517
+ const value = evaluatedValue(body, facts);
6518
+ return BODY_KINDS.has(value.kind) ? writtenReading(shapeOfValue(value), rangeOf(argument)) : absentReading;
6519
+ }
6520
+
5569
6521
  // src/responseStatus.ts
5570
6522
  function statusArgumentOf(call, declaration) {
5571
6523
  const args = readCallArgs(field(call, "arguments"));
@@ -5578,7 +6530,11 @@ function statusArgumentOf(call, declaration) {
5578
6530
  }
5579
6531
  return args.positional[declaration.statusArgument] ?? null;
5580
6532
  }
5581
- function statusNumberOf(node, names, facts) {
6533
+ function statusNumberOf(node, names, facts, bound = /* @__PURE__ */ new Map()) {
6534
+ const passed = node.type === "identifier" ? bound.get(node.text) : void 0;
6535
+ if (passed !== void 0) {
6536
+ return passed;
6537
+ }
5582
6538
  const value = evaluatedValue(node, facts);
5583
6539
  const constant2 = constantOf3(value);
5584
6540
  if (typeof constant2 === "number") {
@@ -5602,21 +6558,42 @@ function declarationOf(node, byName, locals) {
5602
6558
  const name = field(node, "method")?.text;
5603
6559
  return name === void 0 ? void 0 : byName.get(name);
5604
6560
  }
5605
- function collectResponseCalls(node, byName, locals, found) {
5606
- for (const child of node.namedChildren) {
6561
+ function responderOf(node, responders) {
6562
+ const declaration = declarationOf(node, responders.byName, responders.locals);
6563
+ if (declaration !== void 0) {
6564
+ return { kind: "declared", declaration };
6565
+ }
6566
+ const name = receiverlessName(node, responders.locals);
6567
+ const reading = name === null || responders.helper === void 0 ? null : responders.helper(name, node);
6568
+ return reading === null ? void 0 : { kind: "helper", reading };
6569
+ }
6570
+ function receiverlessName(node, locals) {
6571
+ if (node.type === "identifier") {
6572
+ return isBareMethodCall(node, locals) ? node.text : null;
6573
+ }
6574
+ if (node.type !== "call" || field(node, "receiver") !== null) {
6575
+ return null;
6576
+ }
6577
+ return field(node, "method")?.text ?? null;
6578
+ }
6579
+ function readingOfResponder(call, responder, names, options) {
6580
+ return responder.kind === "helper" ? responder.reading : readingOfCall(call, responder.declaration, names, options);
6581
+ }
6582
+ function collectResponseCalls(node, responders, found) {
6583
+ for (const child of children(node)) {
5607
6584
  if (child === null || OWN_BODY_TYPES.has(child.type)) {
5608
6585
  continue;
5609
6586
  }
5610
- if (declarationOf(child, byName, locals) !== void 0) {
6587
+ if (responderOf(child, responders) !== void 0) {
5611
6588
  found.push(child);
5612
6589
  continue;
5613
6590
  }
5614
- collectResponseCalls(child, byName, locals, found);
6591
+ collectResponseCalls(child, responders, found);
5615
6592
  }
5616
6593
  return found;
5617
6594
  }
5618
6595
  function collectReturns(node, found) {
5619
- for (const child of node.namedChildren) {
6596
+ for (const child of children(node)) {
5620
6597
  if (child === null || OWN_BODY_TYPES.has(child.type)) {
5621
6598
  continue;
5622
6599
  }
@@ -5628,19 +6605,24 @@ function collectReturns(node, found) {
5628
6605
  }
5629
6606
  return found;
5630
6607
  }
5631
- function readingOfCall(call, declaration, names, facts) {
6608
+ function readingOfCall(call, declaration, names, options) {
5632
6609
  const argument = statusArgumentOf(call, declaration);
5633
6610
  if (argument === null) {
5634
- return declaration.defaultStatusCode === void 0 ? absentReading : writtenReading(declaration.defaultStatusCode, rangeOf(call));
6611
+ return declaration.defaultStatusCode === void 0 ? absentReading2 : writtenReading2(declaration.defaultStatusCode, rangeOf(call));
5635
6612
  }
5636
- const status = statusNumberOf(argument, names, facts);
6613
+ const status = statusNumberOf(
6614
+ argument,
6615
+ names,
6616
+ options.facts,
6617
+ options.boundStatuses
6618
+ );
5637
6619
  if (status === null) {
5638
6620
  return unreadableReading(
5639
6621
  "This response writes a status that does not settle on a number here, so this outcome claims none",
5640
6622
  rangeOf(argument)
5641
6623
  );
5642
6624
  }
5643
- return writtenReading(status, rangeOf(argument));
6625
+ return writtenReading2(status, rangeOf(argument));
5644
6626
  }
5645
6627
  function effectsReaching(effects, conditions) {
5646
6628
  return effects.filter(
@@ -5669,6 +6651,45 @@ function conditionInputs(method, paths, facts) {
5669
6651
  );
5670
6652
  return operands.length === 0 ? void 0 : guardInputs(operands, { facts, unit: method });
5671
6653
  }
6654
+ function boundStatusArguments(helper, call, pattern, facts) {
6655
+ const parameters = children(field(helper, "parameters")).filter(
6656
+ (child) => child.type === "identifier"
6657
+ );
6658
+ const args = call.type === "call" ? readCallArgs(field(call, "arguments")).positional : [];
6659
+ const bound = /* @__PURE__ */ new Map();
6660
+ parameters.forEach((parameter, index) => {
6661
+ const arg = args[index];
6662
+ const status = arg === void 0 ? null : statusNumberOf(arg, pattern.statusCodeNames ?? {}, facts);
6663
+ if (status !== null) {
6664
+ bound.set(parameter.text, status);
6665
+ }
6666
+ });
6667
+ return bound;
6668
+ }
6669
+ function helperResponse(helper, pattern, facts, boundStatuses = /* @__PURE__ */ new Map()) {
6670
+ const branches = responseBranches(helper, pattern, [], void 0, {
6671
+ fallthrough: "handOn",
6672
+ facts,
6673
+ boundStatuses
6674
+ });
6675
+ if (branches === null || branches.some((branch) => branch.terminal.kind !== "response")) {
6676
+ return null;
6677
+ }
6678
+ const statuses = new Set(
6679
+ branches.map((branch) => {
6680
+ const reading = branch.statusCodeReading?.reading;
6681
+ if (reading?.kind === "absent") {
6682
+ return branch.statusCodeReading?.libraryDefault ?? null;
6683
+ }
6684
+ return reading?.kind === "written" ? reading.value : null;
6685
+ })
6686
+ );
6687
+ const [only] = [...statuses];
6688
+ return statuses.size === 1 && typeof only === "number" ? writtenReading2(only, rangeOf(helper)) : unreadableReading(
6689
+ "This response is sent by a helper whose status does not settle on one number here, so this outcome claims none",
6690
+ rangeOf(helper)
6691
+ );
6692
+ }
5672
6693
  function branchOf(outcome, pattern, effects, extraEffects, options) {
5673
6694
  if (outcome.fellThrough === true && options.fallthrough === "handOn") {
5674
6695
  return handsOnBranch(outcome, effects, extraEffects);
@@ -5691,6 +6712,7 @@ function branchOf(outcome, pattern, effects, extraEffects, options) {
5691
6712
  reading: outcome.reading,
5692
6713
  libraryDefault: pattern.defaultStatusCode
5693
6714
  },
6715
+ ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
5694
6716
  effects: effectsReaching(effects, outcome.conditions),
5695
6717
  ...extraEffects === void 0 ? {} : { extraEffects },
5696
6718
  location: outcome.location,
@@ -5718,20 +6740,26 @@ function handsOnBranch(outcome, effects, extraEffects) {
5718
6740
  isDefault: outcome.conditions.length === 0
5719
6741
  };
5720
6742
  }
5721
- function returnPathBranches(method, effects, exits = [], facts) {
6743
+ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = []) {
5722
6744
  const body = field(method, "body");
5723
6745
  if (body === null) {
5724
6746
  return null;
5725
6747
  }
5726
6748
  const returns = collectReturns(body, []);
5727
6749
  const exitCalls = exits.map((exit) => exit.call);
5728
- const lowered = lowerRubyBody(body, [...returns, ...exitCalls], exitCalls);
6750
+ const terminals = [
6751
+ ...returns,
6752
+ ...exitCalls,
6753
+ ...raises2.map((raise) => raise.call)
6754
+ ];
6755
+ const lowered = lowerRubyBody(body, terminals, exitCalls);
5729
6756
  const enumerated = enumerateOrDegrade2(
5730
6757
  {
5731
6758
  statements: lowered.statements,
5732
- terminalsByStmt: lowered.terminalsByStmt
6759
+ terminalsByStmt: lowered.terminalsByStmt,
6760
+ ...keepsArms === void 0 ? {} : { keepsArms }
5733
6761
  },
5734
- [...returns, ...exitCalls]
6762
+ terminals
5735
6763
  );
5736
6764
  const inputs = conditionInputs(
5737
6765
  method,
@@ -5759,8 +6787,8 @@ function returnPathBranches(method, effects, exits = [], facts) {
5759
6787
  returnTerminal(rangeOf(statement))
5760
6788
  );
5761
6789
  }
5762
- for (const exit of exits) {
5763
- push(enumerated.byTerminal.get(exit.call) ?? [], exit.terminal);
6790
+ for (const ending of [...exits, ...raises2]) {
6791
+ push(enumerated.byTerminal.get(ending.call) ?? [], ending.terminal);
5764
6792
  }
5765
6793
  push(enumerated.fallthrough, returnTerminal(rangeOf(method)));
5766
6794
  return branches.length === 0 ? null : branches;
@@ -5785,13 +6813,22 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5785
6813
  if (declarations.length === 0 || body === null) {
5786
6814
  return null;
5787
6815
  }
5788
- const byName = declarationsByName(declarations);
5789
- const locals = localNamesIn(method);
5790
- const responses = collectResponseCalls(body, byName, locals, []);
6816
+ const responders = {
6817
+ byName: declarationsByName(declarations),
6818
+ locals: localNamesIn(method),
6819
+ helper: options.respondingHelper
6820
+ };
6821
+ const responses = collectResponseCalls(body, responders, []);
5791
6822
  const returns = collectReturns(body, []);
5792
- const lowered = lowerRubyBody(body, returns, responses);
6823
+ const raises2 = options.raises === void 0 ? [] : escapingRaises(method);
6824
+ const lowered = lowerRubyBody(
6825
+ body,
6826
+ [...returns, ...raises2],
6827
+ responses,
6828
+ pattern.formatSwitch
6829
+ );
5793
6830
  const bareReturns = returns.filter(
5794
- (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, byName, locals, []).length === 0
6831
+ (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, responders, []).length === 0
5795
6832
  );
5796
6833
  const terminals = [...responses, ...bareReturns];
5797
6834
  const enumerated = enumerateOrDegrade2(
@@ -5799,7 +6836,7 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5799
6836
  statements: lowered.statements,
5800
6837
  terminalsByStmt: lowered.terminalsByStmt
5801
6838
  },
5802
- terminals
6839
+ [...terminals, ...raises2]
5803
6840
  );
5804
6841
  const statusNames = pattern.statusCodeNames ?? {};
5805
6842
  const inputs = conditionInputs(
@@ -5810,30 +6847,58 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
5810
6847
  const conditionsOf = (path7) => path7.map((condition) => conditionOf(condition, inputs));
5811
6848
  const outcomes = [];
5812
6849
  for (const terminal of terminals) {
5813
- const declaration = declarationOf(terminal, byName, locals);
5814
- const reading = declaration === void 0 ? absentReading : readingOfCall(terminal, declaration, statusNames, options.facts);
6850
+ const responder = responderOf(terminal, responders);
6851
+ const reading = responder === void 0 ? absentReading2 : readingOfResponder(terminal, responder, statusNames, options);
6852
+ const body2 = responder?.kind === "declared" ? bodyReadingOfCall(terminal, responder.declaration, options.facts) : void 0;
5815
6853
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
5816
6854
  outcomes.push({
5817
6855
  conditions: conditionsOf(path7),
5818
6856
  reading,
5819
- location: rangeOf(terminal)
6857
+ ...body2 === void 0 ? {} : { body: body2 },
6858
+ location: rangeOf(terminal),
6859
+ // A bare `return` wrote no response, so a filter that takes it
6860
+ // hands the request on the same as one that reaches its end.
6861
+ ...responder === void 0 ? { fellThrough: true } : {}
5820
6862
  });
5821
6863
  }
5822
6864
  }
5823
6865
  for (const path7 of enumerated.fallthrough) {
5824
6866
  outcomes.push({
5825
6867
  conditions: conditionsOf(path7),
5826
- reading: absentReading,
6868
+ reading: absentReading2,
5827
6869
  location: rangeOf(method),
5828
6870
  fellThrough: true
5829
6871
  });
5830
- }
5831
- if (outcomes.length === 0) {
6872
+ }
6873
+ const read = options.raises;
6874
+ const raised = raises2.flatMap((call) => {
6875
+ const terminal = raisedTerminal(
6876
+ pattern,
6877
+ read?.classes.get(call) ?? null,
6878
+ read?.handlers ?? [],
6879
+ rangeOf(call)
6880
+ );
6881
+ return (enumerated.byTerminal.get(call) ?? []).map((path7) => {
6882
+ const conditions = conditionsOf(path7);
6883
+ return {
6884
+ conditions,
6885
+ terminal,
6886
+ effects: effectsReaching(effects, conditions),
6887
+ ...extraEffects === void 0 ? {} : { extraEffects },
6888
+ location: terminal.location,
6889
+ isDefault: false
6890
+ };
6891
+ });
6892
+ });
6893
+ if (outcomes.length === 0 && raised.length === 0) {
5832
6894
  return null;
5833
6895
  }
5834
- return outcomes.map(
5835
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
5836
- );
6896
+ return [
6897
+ ...outcomes.map(
6898
+ (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
6899
+ ),
6900
+ ...raised
6901
+ ];
5837
6902
  }
5838
6903
 
5839
6904
  // src/clientCalls.ts
@@ -5846,7 +6911,16 @@ function clientCallUnits(root, pack, pattern, options) {
5846
6911
  }
5847
6912
  for (const call of callsUnder2(method)) {
5848
6913
  for (const request of requestCalls(call, pattern, options)) {
5849
- units.push(clientUnit(method, name, request, pattern, pack, options));
6914
+ units.push(
6915
+ clientUnit(
6916
+ method,
6917
+ name,
6918
+ { ...request, call },
6919
+ pattern,
6920
+ pack,
6921
+ options
6922
+ )
6923
+ );
5850
6924
  }
5851
6925
  }
5852
6926
  }
@@ -5904,10 +6978,7 @@ function requestArgument(call, called, pattern) {
5904
6978
  return pattern.requestObject?.attribute === called ? args.positional[0] : void 0;
5905
6979
  }
5906
6980
  function methodDefinitions(node, found = []) {
5907
- for (const child of node.namedChildren) {
5908
- if (child === null) {
5909
- continue;
5910
- }
6981
+ for (const child of children(node)) {
5911
6982
  if (child.type === "method" || child.type === "singleton_method") {
5912
6983
  found.push(child);
5913
6984
  }
@@ -5916,10 +6987,7 @@ function methodDefinitions(node, found = []) {
5916
6987
  return found;
5917
6988
  }
5918
6989
  function callsUnder2(node, found = []) {
5919
- for (const child of node.namedChildren) {
5920
- if (child === null) {
5921
- continue;
5922
- }
6990
+ for (const child of children(node)) {
5923
6991
  if (child.type === "call") {
5924
6992
  found.push(child);
5925
6993
  }
@@ -5936,7 +7004,7 @@ function requestCalls(call, pattern, options) {
5936
7004
  const read = (site) => requestCall(
5937
7005
  call,
5938
7006
  called,
5939
- receiverPrefix(receiver, pattern, options, site),
7007
+ receiverBase(receiver, pattern, options, site),
5940
7008
  pattern,
5941
7009
  options,
5942
7010
  site
@@ -5949,7 +7017,10 @@ function requestCalls(call, pattern, options) {
5949
7017
  for (const site of constructionSitesOf(call, options.facts)) {
5950
7018
  const stated = read(site);
5951
7019
  if (stated !== null) {
5952
- byBoundary.set(`${stated.method} ${stated.path}`, stated);
7020
+ byBoundary.set(
7021
+ `${stated.method} ${stated.host ?? ""} ${stated.path}`,
7022
+ stated
7023
+ );
5953
7024
  }
5954
7025
  }
5955
7026
  if (byBoundary.size > 0) {
@@ -5957,12 +7028,12 @@ function requestCalls(call, pattern, options) {
5957
7028
  }
5958
7029
  return plain === null ? [] : [plain];
5959
7030
  }
5960
- function requestCall(call, called, prefix, pattern, options, site) {
7031
+ function requestCall(call, called, base, pattern, options, site) {
5961
7032
  const args = readCallArgs(field(call, "arguments"));
5962
7033
  const verb = pattern.verbMethodNames[called];
5963
7034
  if (verb !== void 0) {
5964
- const path8 = urlIn(args, pattern, options, site);
5965
- return path8 === null ? null : { method: verb, path: prefix + path8 };
7035
+ const target2 = targetAt(urlNodeIn(args, pattern), options, site);
7036
+ return target2 === null ? null : { method: verb, ...underBase(base, target2) };
5966
7037
  }
5967
7038
  const sent = pattern.requestObject;
5968
7039
  if (sent === void 0 || sent.attribute !== called) {
@@ -5972,8 +7043,14 @@ function requestCall(call, called, prefix, pattern, options, site) {
5972
7043
  if (built === null) {
5973
7044
  return null;
5974
7045
  }
5975
- const path7 = pathAt(built.url, options, site);
5976
- return path7 === null ? null : { method: built.method, path: prefix + path7 };
7046
+ const target = targetAt(built.url, options, site);
7047
+ return target === null ? null : { method: built.method, ...underBase(base, target) };
7048
+ }
7049
+ function underBase(base, target) {
7050
+ if (base === null || target.host !== void 0) {
7051
+ return target;
7052
+ }
7053
+ return { path: base.path + target.path, host: base.host };
5977
7054
  }
5978
7055
  function requestBuilt(argument, sent, options, site) {
5979
7056
  if (argument === void 0) {
@@ -5994,16 +7071,19 @@ function requestBuilt(argument, sent, options, site) {
5994
7071
  function isLibraryReceiver(receiver, pattern, options) {
5995
7072
  return namesConstant2(receiver, pattern.constantName) || builderCallBehind(receiver, pattern, options.facts) !== null;
5996
7073
  }
5997
- function receiverPrefix(receiver, pattern, options, site) {
7074
+ function receiverBase(receiver, pattern, options, site) {
5998
7075
  const built = builderCallBehind(receiver, pattern, options.facts);
5999
7076
  if (built === null) {
6000
- return "";
7077
+ return null;
6001
7078
  }
6002
7079
  const args = readCallArgs(field(built, "arguments"));
6003
7080
  const keyword = pattern.builderUrlKeyword;
6004
7081
  const base = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[0];
6005
- const path7 = base === void 0 ? null : pathOf(evaluatedValue(base, options.facts, void 0, site));
6006
- return path7 === void 0 || path7 === null ? "" : trimmed(path7);
7082
+ if (base === void 0) {
7083
+ return null;
7084
+ }
7085
+ const value = evaluatedValue(base, options.facts, void 0, site);
7086
+ return { path: trimmed(pathOf(value) ?? ""), host: hostOf(value) };
6007
7087
  }
6008
7088
  function trimmed(path7) {
6009
7089
  return path7 === "/" ? "" : path7.replace(/\/+$/, "");
@@ -6026,22 +7106,32 @@ function builderCallBehind(receiver, pattern, facts) {
6026
7106
  const constant2 = field(written, "receiver");
6027
7107
  return constant2 !== null && namesConstant2(constant2, pattern.constantName) ? written : null;
6028
7108
  }
6029
- function urlIn(args, pattern, options, site) {
6030
- return pathAt(urlNodeIn(args, pattern), options, site);
6031
- }
6032
7109
  function urlNodeIn(args, pattern) {
6033
7110
  const keyword = pattern.url.keyword;
6034
7111
  return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
6035
7112
  }
6036
- function pathAt(written, options, site) {
7113
+ function targetAt(written, options, site) {
6037
7114
  if (written === void 0) {
6038
7115
  return null;
6039
7116
  }
6040
- const path7 = pathOf(evaluatedValue(written, options.facts, void 0, site));
6041
- return path7 === void 0 || namesNothing(path7) ? null : path7;
7117
+ const value = evaluatedValue(written, options.facts, void 0, site);
7118
+ const path7 = pathOf(value);
7119
+ return path7 === void 0 || namesNothing(path7) ? null : { path: path7, host: hostOf(value) };
6042
7120
  }
6043
7121
  function clientUnit(method, name, request, pattern, pack, options) {
6044
7122
  const range = rangeOf(method);
7123
+ const accessors = responseAccessors(pattern);
7124
+ const statusMembers = statusMembersOf(accessors);
7125
+ const reading = {
7126
+ facts: options.facts,
7127
+ statusMembers,
7128
+ handOff: {
7129
+ request: request.call,
7130
+ bodyMethods: accessors.bodyAccessors ?? [],
7131
+ facts: options.facts,
7132
+ readHelper: (helper) => callerBranches(helper, { facts: options.facts, statusMembers })
7133
+ }
7134
+ };
6045
7135
  return {
6046
7136
  identity: {
6047
7137
  name,
@@ -6057,21 +7147,30 @@ function clientUnit(method, name, request, pattern, pack, options) {
6057
7147
  transport: pack.protocol,
6058
7148
  method: request.method,
6059
7149
  path: request.path,
7150
+ host: request.host,
6060
7151
  recognition: pack.name
6061
7152
  }),
6062
7153
  parameters: [],
6063
- branches: callerBranches(method, range, options.facts),
6064
- ...responseAccessors(pattern),
7154
+ branches: callerBranches(method, reading),
7155
+ ...accessors,
6065
7156
  bodyContent: "statements",
6066
7157
  dependencyCalls: [],
6067
7158
  declaredContract: null
6068
7159
  };
6069
7160
  }
6070
- function callerBranches(method, range, facts) {
6071
- const effects = invocationEffects(method, void 0, void 0, facts);
6072
- return returnPathBranches(method, effects, [], facts) ?? [
6073
- returnBranch(range, effects)
6074
- ];
7161
+ function callerBranches(method, reading) {
7162
+ const sites = invocationSites(method, void 0, void 0, reading.facts);
7163
+ const effects = sites.map((site) => site.effect);
7164
+ const keepsArms = (condition) => testsStatus(predicateOf(condition), reading.statusMembers);
7165
+ const found = returnPathBranches(
7166
+ method,
7167
+ effects,
7168
+ [],
7169
+ reading.facts,
7170
+ keepsArms,
7171
+ raiseEndings(method)
7172
+ ) ?? [returnBranch(rangeOf(method), effects)];
7173
+ return reading.handOff === void 0 ? found : throughResponseHelpers(found, sites, reading.handOff);
6075
7174
  }
6076
7175
  function responseAccessors(pattern) {
6077
7176
  const response = pattern.response;
@@ -6082,7 +7181,8 @@ function responseAccessors(pattern) {
6082
7181
  ...response.body === void 0 ? {} : { bodyAccessors: response.body },
6083
7182
  ...response.statusCode === void 0 ? {} : { statusAccessors: response.statusCode },
6084
7183
  ...response.success === void 0 ? {} : { successAccessors: response.success },
6085
- ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery }
7184
+ ...response.failureDelivery === void 0 ? {} : { failureDelivery: response.failureDelivery },
7185
+ ...response.redirectDelivery === void 0 ? {} : { redirectDelivery: response.redirectDelivery }
6086
7186
  };
6087
7187
  }
6088
7188
  function returnBranch(range, effects) {
@@ -6133,9 +7233,7 @@ function elementSite(node) {
6133
7233
  if (object === null || !isEnv(object) || isAssignedTo(node)) {
6134
7234
  return null;
6135
7235
  }
6136
- const index = node.namedChildren.find(
6137
- (child) => child !== null && child.id !== object.id
6138
- );
7236
+ const index = children(node).find((child) => child.id !== object.id);
6139
7237
  if (index === void 0) {
6140
7238
  return null;
6141
7239
  }
@@ -6316,7 +7414,7 @@ function followsExitWhenAbsent(parent, child, subject) {
6316
7414
  if (!STATEMENT_LIST_TYPES.has(parent.type)) {
6317
7415
  return false;
6318
7416
  }
6319
- const statements = parent.namedChildren;
7417
+ const statements = children(parent);
6320
7418
  const at = statements.findIndex((statement) => statement?.id === child.id);
6321
7419
  return statements.slice(0, at).some(
6322
7420
  (statement) => statement !== null && leavesWhenAbsent(statement, subject)
@@ -6368,7 +7466,7 @@ function continuationRaises(from) {
6368
7466
  let parent = current.parent;
6369
7467
  while (parent !== null) {
6370
7468
  if (STATEMENT_LIST_TYPES.has(parent.type)) {
6371
- const statements = parent.namedChildren;
7469
+ const statements = children(parent);
6372
7470
  const at = statements.findIndex((one) => one?.id === current.id);
6373
7471
  const exit = firstExit(statements.slice(at + 1));
6374
7472
  if (exit !== null) {
@@ -6402,20 +7500,13 @@ function firstExit(statements) {
6402
7500
  return null;
6403
7501
  }
6404
7502
  function exitOf2(statement) {
6405
- if (isRaise3(statement)) {
7503
+ if (isRaiseCall(statement)) {
6406
7504
  return "raise";
6407
7505
  }
6408
7506
  if (LEAVING_TYPES.has(statement.type)) {
6409
7507
  return "leave";
6410
7508
  }
6411
- return STATEMENT_LIST_TYPES.has(statement.type) ? firstExit(statement.namedChildren) : null;
6412
- }
6413
- var RAISING_METHODS2 = /* @__PURE__ */ new Set(["raise", "fail"]);
6414
- function isRaise3(node) {
6415
- if (node.type === "identifier") {
6416
- return RAISING_METHODS2.has(node.text);
6417
- }
6418
- return node.type === "call" && field(node, "receiver") === null && RAISING_METHODS2.has(field(node, "method")?.text ?? "");
7509
+ return STATEMENT_LIST_TYPES.has(statement.type) ? firstExit(children(statement)) : null;
6419
7510
  }
6420
7511
  function isPresentWhen(test, outcome, subject) {
6421
7512
  const expression = peelParens(test);
@@ -6483,7 +7574,7 @@ function isNilCheckOn(call, receiver) {
6483
7574
  return call.type === "call" && field(call, "method")?.text === "nil?" && field(call, "receiver")?.id === receiver.id;
6484
7575
  }
6485
7576
  function peelParens(node) {
6486
- const inner = node.type === "parenthesized_statements" && node.namedChildCount === 1 ? node.namedChildren[0] : null;
7577
+ const inner = node.type === "parenthesized_statements" && node.namedChildCount === 1 ? node.namedChild(0) : null;
6487
7578
  return inner === null || inner === void 0 ? node : peelParens(inner);
6488
7579
  }
6489
7580
  function climbParens(node) {
@@ -6528,7 +7619,7 @@ var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method",
6528
7619
  function envReadEffects(root, facts) {
6529
7620
  const helpers = helperFactsOf(facts);
6530
7621
  const slots = [];
6531
- walkDescendants2(root, null, {
7622
+ walkDescendants2(root, null, children, {
6532
7623
  at: (node) => {
6533
7624
  const read = envReadAt(node);
6534
7625
  if (read !== null) {
@@ -6608,7 +7699,7 @@ function namedParameters(db) {
6608
7699
  return named2;
6609
7700
  }
6610
7701
  function emitEnvFacts(db, file, root) {
6611
- walkDescendants2(root, null, {
7702
+ walkDescendants2(root, null, children, {
6612
7703
  at: (node) => {
6613
7704
  if (isEnv(node) && handsOnward(node)) {
6614
7705
  db.add("environmentObject", [
@@ -6736,7 +7827,8 @@ function envSiteAt(node) {
6736
7827
  }
6737
7828
 
6738
7829
  // src/filters.ts
6739
- function controllerFilters(pattern, ancestry, read = {}) {
7830
+ var RESCUES_NOTHING = { refs: [], someUnread: false };
7831
+ function controllerFilterChain(pattern, ancestry, read = {}) {
6740
7832
  const forms = pattern.filters ?? [];
6741
7833
  if (forms.length === 0) {
6742
7834
  return [];
@@ -6749,18 +7841,29 @@ function controllerFilters(pattern, ancestry, read = {}) {
6749
7841
  }
6750
7842
  }
6751
7843
  let declared = [];
6752
- for (const { statement } of inheritedStatements(ancestry)) {
7844
+ const inlineSeen = /* @__PURE__ */ new Map();
7845
+ for (const { block, statement } of inheritedStatements(ancestry)) {
6753
7846
  const called = calledName(statement);
6754
7847
  if (called === null) {
6755
7848
  continue;
6756
7849
  }
6757
7850
  const form = byName.get(called);
6758
7851
  if (form !== void 0) {
6759
- for (const declaration of declarationsOf(statement, form, read.facts)) {
6760
- declared = [
6761
- ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
6762
- declaration
6763
- ];
7852
+ const inlineKey = `${block.file}\0${block.info.qualifiedName}\0${form.name}`;
7853
+ const ordinal = inlineSeen.get(inlineKey) ?? 0;
7854
+ const declarations = declarationsOf(
7855
+ statement,
7856
+ form,
7857
+ block,
7858
+ read.facts,
7859
+ ordinal
7860
+ );
7861
+ if (declarations.some((one) => one.inline !== void 0)) {
7862
+ inlineSeen.set(inlineKey, ordinal + 1);
7863
+ }
7864
+ for (const declaration of declarations) {
7865
+ const earlier = declaration.filter.onThrow === true ? void 0 : declared.find((one) => sameFilter(one, declaration));
7866
+ declared = [...declared.filter((one) => one !== earlier), declaration];
6764
7867
  }
6765
7868
  continue;
6766
7869
  }
@@ -6769,26 +7872,78 @@ function controllerFilters(pattern, ancestry, read = {}) {
6769
7872
  declared = applySkips(declared, skipsOf(statement, skipped, read.facts));
6770
7873
  }
6771
7874
  }
6772
- const resolved = [];
6773
- for (const declaration of declared) {
6774
- const found = methodInAncestry(ancestry, declaration.methodName, read);
6775
- if (found.type !== "found") {
6776
- continue;
6777
- }
6778
- resolved.push({
7875
+ const resolved = declared.map(
7876
+ (declaration) => linkFor(
7877
+ declaration,
7878
+ writtenAt(
7879
+ declaration,
7880
+ filterMethod(ancestry, declaration.methodName, read)
7881
+ )
7882
+ )
7883
+ );
7884
+ return [
7885
+ ...resolved.filter((one) => !runsAfterARaise(one)),
7886
+ ...resolved.filter(runsAfterARaise).reverse()
7887
+ ];
7888
+ }
7889
+ function linkFor(declaration, written) {
7890
+ if (written === null) {
7891
+ return {
7892
+ unread: {
7893
+ filter: declaration.filter,
7894
+ methodName: declaration.methodName,
7895
+ declaredIn: declaration.declaredIn,
7896
+ only: declaration.only,
7897
+ except: declaration.except
7898
+ }
7899
+ };
7900
+ }
7901
+ return {
7902
+ read: {
6779
7903
  filter: declaration.filter,
7904
+ ...written,
7905
+ only: declaration.only,
7906
+ except: declaration.except,
7907
+ rescues: declaration.rescues
7908
+ }
7909
+ };
7910
+ }
7911
+ function filterOf(link) {
7912
+ return "read" in link ? link.read : link.unread;
7913
+ }
7914
+ function runsAfterARaise(link) {
7915
+ return filterOf(link).filter.onThrow === true;
7916
+ }
7917
+ function writtenAt(declaration, found) {
7918
+ if (found.type === "found") {
7919
+ return {
6780
7920
  methodName: declaration.methodName,
7921
+ nameKind: "binding",
6781
7922
  method: found.method,
6782
7923
  file: found.block.file,
6783
7924
  enclosingQualifiedName: found.block.info.qualifiedName,
6784
- only: declaration.only,
6785
- except: declaration.except
6786
- });
7925
+ nesting: found.block.info.bodyNesting
7926
+ };
6787
7927
  }
6788
- return [
6789
- ...resolved.filter((one) => one.filter.onThrow !== true),
6790
- ...resolved.filter((one) => one.filter.onThrow === true)
6791
- ];
7928
+ const inline = declaration.inline;
7929
+ if (inline === void 0) {
7930
+ return null;
7931
+ }
7932
+ return {
7933
+ methodName: inline.label,
7934
+ nameKind: "label",
7935
+ method: inline.block,
7936
+ file: inline.declaredIn.file,
7937
+ enclosingQualifiedName: inline.declaredIn.info.qualifiedName,
7938
+ nesting: inline.declaredIn.info.bodyNesting
7939
+ };
7940
+ }
7941
+ function filterMethod(ancestry, name, read) {
7942
+ const found = methodInAncestry(ancestry, name, read);
7943
+ if (found.type === "unsettled" && found.cause === "unreadAncestor") {
7944
+ return methodPastUnreadAncestors(ancestry, name, read);
7945
+ }
7946
+ return found;
6792
7947
  }
6793
7948
  function filterCoversAction(filter, actionName) {
6794
7949
  if (filter.except.has(actionName)) {
@@ -6796,37 +7951,55 @@ function filterCoversAction(filter, actionName) {
6796
7951
  }
6797
7952
  return filter.only === null || filter.only.has(actionName);
6798
7953
  }
6799
- function filterReference(filter, displayPath) {
7954
+ function filterReference(filter, displayPath, caught) {
7955
+ return {
7956
+ file: displayPath,
7957
+ name: filter.methodName,
7958
+ ...filter.filter.onThrow === true ? { onThrow: true } : {},
7959
+ ...caught === void 0 ? {} : {
7960
+ catches: caught.classes.map((one) => one.name),
7961
+ ...caught.classes.some((one) => one.inheritableByUnread) ? { mayCatchUnreadClasses: true } : {},
7962
+ ...caught.someUnread ? { mayCatchAny: true } : {}
7963
+ }
7964
+ };
7965
+ }
7966
+ function unreadFilterReference(filter, displayPath) {
6800
7967
  return {
6801
7968
  file: displayPath,
6802
7969
  name: filter.methodName,
6803
7970
  ...filter.filter.onThrow === true ? { onThrow: true } : {}
6804
7971
  };
6805
7972
  }
6806
- function filterUnit(filter, pattern, displayPath, body, facts) {
7973
+ function filterUnit(filter, pattern, displayPath, body, reading = {}) {
6807
7974
  const range = rangeOf(filter.method);
7975
+ const { respondingHelper, raises: raises2 } = reading;
6808
7976
  const branches = responseBranches(
6809
7977
  filter.method,
6810
7978
  pattern,
6811
7979
  body.effects ?? [],
6812
7980
  body.extraEffects,
6813
- { fallthrough: "handOn", facts }
7981
+ {
7982
+ fallthrough: "handOn",
7983
+ facts: reading.facts,
7984
+ ...respondingHelper === void 0 ? {} : { respondingHelper },
7985
+ ...raises2 === void 0 ? {} : { raises: raises2 }
7986
+ }
6814
7987
  );
6815
7988
  return {
6816
7989
  identity: {
6817
7990
  name: filter.methodName,
6818
- nameKind: "binding",
7991
+ nameKind: filter.nameKind,
6819
7992
  kind: "middleware",
6820
7993
  file: displayPath,
6821
7994
  range,
6822
7995
  span: spanOf(filter.method),
6823
- exportName: filter.methodName,
7996
+ exportName: filter.nameKind === "binding" ? filter.methodName : null,
6824
7997
  exportPath: [filter.enclosingQualifiedName, filter.methodName]
6825
7998
  },
6826
7999
  boundaryBinding: null,
6827
8000
  parameters: [],
6828
8001
  branches: withSlotSources(
6829
- branches ?? [handsOn(range, body)],
8002
+ [...branches ?? [handsOn(range, body)], ...reading.raised ?? []],
6830
8003
  body.provenance
6831
8004
  ),
6832
8005
  bodyContent: body.bodyContent ?? "absent",
@@ -6861,25 +8034,93 @@ function calledName(statement) {
6861
8034
  }
6862
8035
  return field(statement, "method")?.text ?? null;
6863
8036
  }
6864
- function declarationsOf(statement, filter, facts) {
8037
+ function declarationsOf(statement, filter, declaredIn, facts, inlineOrdinal) {
6865
8038
  const args = readCallArgs(field(statement, "arguments"));
6866
8039
  const keywords = filter.actionKeywords;
6867
8040
  const only = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.include], facts);
6868
8041
  const except = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.exclude], facts);
6869
- return methodNamesOf(args, filter, facts).map((methodName) => ({
8042
+ const rescues2 = filter.onThrow === true ? rescuedClasses(args, declaredIn.info.bodyNesting, facts) : RESCUES_NOTHING;
8043
+ const inline = inlineHandler(
8044
+ statement,
8045
+ args,
8046
+ filter,
8047
+ declaredIn,
8048
+ inlineOrdinal
8049
+ );
8050
+ return methodNamesOf(args, filter, facts, inline).map((methodName) => ({
6870
8051
  filter,
6871
8052
  methodName,
6872
8053
  only,
6873
- except: except ?? /* @__PURE__ */ new Set()
8054
+ except: except ?? /* @__PURE__ */ new Set(),
8055
+ rescues: rescues2,
8056
+ declaredIn: declaredIn.file,
8057
+ ...inline === void 0 ? {} : { inline }
6874
8058
  }));
6875
8059
  }
6876
- function methodNamesOf(args, filter, facts) {
8060
+ function inlineHandler(statement, args, filter, declaredIn, ordinal) {
8061
+ const block = blockInPlaceOfMethod(statement, args, filter);
8062
+ if (block === null || blockStatements2(block).length === 0) {
8063
+ return void 0;
8064
+ }
8065
+ return { block, label: `${filter.name}#${ordinal}`, declaredIn };
8066
+ }
8067
+ function rescuedClasses(args, nesting, facts) {
8068
+ const refs = [];
8069
+ let someUnread = args.positional.length === 0;
8070
+ for (const arg of args.positional) {
8071
+ const candidates = constantRefCandidates(arg, nesting);
8072
+ const named2 = candidates.length === 0 ? stringValueOf(arg, facts) : null;
8073
+ if (candidates.length === 0 && named2 === null) {
8074
+ someUnread = true;
8075
+ continue;
8076
+ }
8077
+ refs.push({
8078
+ text: arg.text,
8079
+ candidates: named2 === null ? candidates : [named2]
8080
+ });
8081
+ }
8082
+ return { refs, someUnread };
8083
+ }
8084
+ function methodNamesOf(args, filter, facts, inline) {
8085
+ const named2 = filter.methodFrom === "withKeyword" ? withKeywordName(args, facts) : symbolArgumentNames(args, facts);
8086
+ if (named2.length > 0 || inline === void 0) {
8087
+ return named2;
8088
+ }
8089
+ return [blockHandlerName(inline.block) ?? inline.label];
8090
+ }
8091
+ function withKeywordName(args, facts) {
8092
+ const named2 = args.keyword.with;
8093
+ const value = named2 === void 0 ? null : stringValueOf(named2, facts);
8094
+ return value === null ? [] : [value];
8095
+ }
8096
+ function blockInPlaceOfMethod(statement, args, filter) {
6877
8097
  if (filter.methodFrom === "withKeyword") {
6878
- const named2 = args.keyword.with;
6879
- const value = named2 === void 0 ? null : stringValueOf(named2, facts);
6880
- return value === null ? [] : [value];
8098
+ return args.keyword.with === void 0 ? field(statement, "block") : null;
8099
+ }
8100
+ const lambda = args.positional.find((arg) => arg.type === "lambda");
8101
+ if (lambda !== void 0) {
8102
+ return field(lambda, "body");
8103
+ }
8104
+ return args.positional.length === 0 ? field(statement, "block") : null;
8105
+ }
8106
+ function blockStatements2(block) {
8107
+ return children(field(block, "body")).filter(
8108
+ (child) => child.type !== "comment"
8109
+ );
8110
+ }
8111
+ function blockHandlerName(block) {
8112
+ const statements = blockStatements2(block);
8113
+ const only = statements.length === 1 ? statements[0] : void 0;
8114
+ if (only === void 0) {
8115
+ return null;
8116
+ }
8117
+ const parameters = new Set(
8118
+ children(field(block, "parameters")).map((parameter) => parameter.text)
8119
+ );
8120
+ if (only.type === "identifier") {
8121
+ return parameters.has(only.text) ? null : only.text;
6881
8122
  }
6882
- return symbolArgumentNames(args, facts);
8123
+ return calledName(only);
6883
8124
  }
6884
8125
  function symbolArgumentNames(args, facts) {
6885
8126
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
@@ -6890,13 +8131,16 @@ function skipsOf(statement, filterName, facts) {
6890
8131
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
6891
8132
  }
6892
8133
  function sameFilter(one, other) {
6893
- return one.filter.name === other.filter.name && one.methodName === other.methodName;
8134
+ return namedBySymbol(one) && namedBySymbol(other) && one.filter.name === other.filter.name && one.methodName === other.methodName;
8135
+ }
8136
+ function namedBySymbol(declaration) {
8137
+ return declaration.inline === void 0;
6894
8138
  }
6895
8139
  function applySkips(declared, skips) {
6896
8140
  const left = [];
6897
8141
  for (const declaration of declared) {
6898
8142
  const reaching = skips.filter(
6899
- (skip) => skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
8143
+ (skip) => namedBySymbol(declaration) && skip.filterName === declaration.filter.name && skip.methodName === declaration.methodName
6900
8144
  );
6901
8145
  if (reaching.some((skip) => skip.actions === null)) {
6902
8146
  continue;
@@ -6939,7 +8183,7 @@ var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
6939
8183
  var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
6940
8184
  function streamWriteEffects(root) {
6941
8185
  const found = [];
6942
- walkDescendants3(root, null, {
8186
+ walkDescendants3(root, null, children, {
6943
8187
  at: (node) => {
6944
8188
  const write = node.type === "call" ? streamWriteAt(node) : null;
6945
8189
  if (write !== null) {
@@ -7030,7 +8274,7 @@ function writtenShapeOf(node) {
7030
8274
  return hashShape(node);
7031
8275
  }
7032
8276
  if (node.type === "array") {
7033
- const first = node.namedChildren.find((child) => child !== null);
8277
+ const first = children(node).find((child) => child !== null);
7034
8278
  return {
7035
8279
  type: "array",
7036
8280
  items: first ? writtenShapeOf(first) : { type: "unknown" }
@@ -7040,7 +8284,7 @@ function writtenShapeOf(node) {
7040
8284
  }
7041
8285
  function hashShape(node) {
7042
8286
  const properties = {};
7043
- for (const pair of node.namedChildren) {
8287
+ for (const pair of children(node)) {
7044
8288
  if (pair?.type !== "pair") {
7045
8289
  continue;
7046
8290
  }
@@ -7403,14 +8647,14 @@ var NodesSeen = class {
7403
8647
  };
7404
8648
  function exampleRun(example, pattern, shared) {
7405
8649
  const byName = /* @__PURE__ */ new Map();
7406
- const always2 = [];
8650
+ const always3 = [];
7407
8651
  for (const group of withInlineIncludes(example.groups, pattern, shared)) {
7408
8652
  for (const one of groupBlocks(group, pattern)) {
7409
8653
  for (const name of one.names) {
7410
8654
  byName.set(name, one.block);
7411
8655
  }
7412
8656
  if (one.always) {
7413
- always2.push(one.block);
8657
+ always3.push(one.block);
7414
8658
  }
7415
8659
  }
7416
8660
  }
@@ -7429,7 +8673,7 @@ function exampleRun(example, pattern, shared) {
7429
8673
  }
7430
8674
  };
7431
8675
  include(example.block);
7432
- for (const block of always2) {
8676
+ for (const block of always3) {
7433
8677
  include(block);
7434
8678
  }
7435
8679
  const valueNames = /* @__PURE__ */ new Set([
@@ -7802,8 +9046,8 @@ function moduleOfConstant(node, options) {
7802
9046
  const files = [...new Set(keys.map(fileOfKey))];
7803
9047
  return files.length === 1 ? options.displayPathOf(files[0]) : null;
7804
9048
  }
7805
- function oneLine(text2) {
7806
- return text2.replace(/\s*\n\s*/g, " ").trim();
9049
+ function oneLine(text3) {
9050
+ return text3.replace(/\s*\n\s*/g, " ").trim();
7807
9051
  }
7808
9052
 
7809
9053
  // src/testClasses.ts
@@ -8074,6 +9318,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
8074
9318
  },
8075
9319
  lookup: {
8076
9320
  root: pattern.root,
9321
+ autoloadRoots: pattern.autoloadRoots ?? [],
8077
9322
  pathConvention: pattern.pathConvention,
8078
9323
  acronyms: pattern.acronyms ?? [],
8079
9324
  ancestryRootClassNames: pattern.ancestryRootClassNames,
@@ -8229,6 +9474,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8229
9474
  }
8230
9475
  const lookup = {
8231
9476
  root: pattern.root,
9477
+ autoloadRoots: pattern.autoloadRoots ?? [],
8232
9478
  pathConvention: pattern.pathConvention,
8233
9479
  acronyms: pattern.acronyms ?? [],
8234
9480
  ancestryRootClassNames: pattern.ancestryRootClassNames,
@@ -8243,11 +9489,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8243
9489
  ])) {
8244
9490
  return [];
8245
9491
  }
8246
- const filters = controllerFilters(pattern, ancestry, {
9492
+ const chain = controllerFilterChain(pattern, ancestry, {
8247
9493
  facts: options.facts,
8248
9494
  bodyBlocks: options.bodyBlocks,
8249
9495
  dynamicNames: options.dynamicNames
8250
9496
  });
9497
+ const filters = chain.flatMap((link) => "read" in link ? [link.read] : []);
8251
9498
  const units = [];
8252
9499
  const actions = controllerActions(
8253
9500
  pattern,
@@ -8267,15 +9514,41 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8267
9514
  options.facts,
8268
9515
  options.storage
8269
9516
  );
9517
+ const respondingHelper = respondingHelpersOf(ancestry, pattern, options);
9518
+ const exceptions = new ExceptionReader(
9519
+ lookup,
9520
+ pattern.libraryExceptions ?? {}
9521
+ );
9522
+ const caught = await handlerClasses(filters, exceptions);
9523
+ const handlersAround = (actionName) => filters.filter((filter) => filterCoversAction(filter, actionName)).flatMap((filter) => {
9524
+ const classes = caught.get(filter);
9525
+ return classes === void 0 ? [] : [classes];
9526
+ });
8270
9527
  for (const filter of filters) {
8271
- const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
8272
- const raw = filterUnit(
8273
- filter,
8274
- pattern,
8275
- displayPath,
8276
- bodyOfMethod(filter.method, filter.file, options),
8277
- options.facts
8278
- );
9528
+ const displayPath2 = options.displayPathOf?.(filter.file) ?? filter.file;
9529
+ const body = bodyOfMethod(filter.method, filter.file, options);
9530
+ const handler = filter.filter.onThrow === true;
9531
+ const raw = filterUnit(filter, pattern, displayPath2, body, {
9532
+ facts: options.facts,
9533
+ respondingHelper,
9534
+ ...handler ? {} : {
9535
+ raises: {
9536
+ classes: await raisedClasses(
9537
+ filter.method,
9538
+ filter.nesting,
9539
+ exceptions
9540
+ ),
9541
+ handlers: "eachRoute"
9542
+ },
9543
+ raised: raisedStatusBranches(
9544
+ pattern,
9545
+ options.storage?.patterns ?? [],
9546
+ body.extraEffects,
9547
+ "eachRoute",
9548
+ rangeOf(filter.method)
9549
+ )
9550
+ }
9551
+ });
8279
9552
  units.push(raw);
8280
9553
  options.onReachSeed?.(raw, {
8281
9554
  file: filter.file,
@@ -8283,38 +9556,71 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
8283
9556
  enclosingQualifiedName: filter.enclosingQualifiedName
8284
9557
  });
8285
9558
  }
8286
- const emitAction = (actionName, method, block) => {
8287
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
8288
- (filter) => filterReference(
8289
- filter,
8290
- options.displayPathOf?.(filter.file) ?? filter.file
8291
- )
8292
- );
8293
- const raw = buildControllerActionUnit(
8294
- pack,
8295
- pattern,
8296
- info.qualifiedName,
8297
- actionName,
8298
- method,
8299
- {
8300
- display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
8301
- absolute: block.file
8302
- },
8303
- options,
8304
- around
8305
- );
8306
- units.push(raw);
8307
- options.onReachSeed?.(raw, {
8308
- file: block.file,
8309
- node: method,
8310
- enclosingQualifiedName: block.info.qualifiedName
8311
- });
9559
+ const displayPath = (file) => options.displayPathOf?.(file) ?? file;
9560
+ const referenceTo = (link) => "read" in link ? filterReference(
9561
+ link.read,
9562
+ displayPath(link.read.file),
9563
+ caught.get(link.read)
9564
+ ) : unreadFilterReference(link.unread, displayPath(link.unread.declaredIn));
9565
+ const emitAction = async (actionName, method, block, routes) => {
9566
+ const around = chain.filter((link) => filterCoversAction(filterOf(link), actionName)).map((link) => referenceTo(link));
9567
+ const raises2 = {
9568
+ classes: await raisedClasses(method, block.info.bodyNesting, exceptions),
9569
+ handlers: handlersAround(actionName)
9570
+ };
9571
+ for (const route of routes.length === 0 ? [null] : routes) {
9572
+ const raw = buildControllerActionUnit(
9573
+ pack,
9574
+ pattern,
9575
+ info.qualifiedName,
9576
+ actionName,
9577
+ route,
9578
+ method,
9579
+ {
9580
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
9581
+ absolute: block.file
9582
+ },
9583
+ options,
9584
+ around,
9585
+ respondingHelper,
9586
+ raises2
9587
+ );
9588
+ units.push(raw);
9589
+ options.onReachSeed?.(raw, {
9590
+ file: block.file,
9591
+ node: method,
9592
+ enclosingQualifiedName: block.info.qualifiedName
9593
+ });
9594
+ }
8312
9595
  };
8313
9596
  for (const [actionName, method, block] of actions) {
8314
- emitAction(actionName, method, block);
9597
+ const routes = pattern.routesFor(info.qualifiedName, actionName);
9598
+ await emitAction(actionName, method, block, routes);
8315
9599
  }
8316
9600
  return units;
8317
9601
  }
9602
+ async function handlerClasses(filters, exceptions) {
9603
+ const caught = /* @__PURE__ */ new Map();
9604
+ for (const filter of filters) {
9605
+ if (filter.filter.onThrow !== true) {
9606
+ continue;
9607
+ }
9608
+ const classes = [];
9609
+ for (const ref of filter.rescues.refs) {
9610
+ classes.push(await exceptions.read(ref));
9611
+ }
9612
+ caught.set(filter, { classes, someUnread: filter.rescues.someUnread });
9613
+ }
9614
+ return caught;
9615
+ }
9616
+ async function raisedClasses(method, nesting, exceptions) {
9617
+ const classes = new NodeMap();
9618
+ for (const call of escapingRaises(method)) {
9619
+ const ref = raisedClassRef(call, nesting);
9620
+ classes.set(call, ref === null ? null : await exceptions.read(ref));
9621
+ }
9622
+ return classes;
9623
+ }
8318
9624
  function controllerActions(pattern, info, ownBlocks, ancestry, options) {
8319
9625
  const actions = publicInstanceMethods(ownBlocks, options.bodyBlocks);
8320
9626
  const seen = new Set(actions.map(([actionName]) => actionName));
@@ -8331,7 +9637,7 @@ function controllerActions(pattern, info, ownBlocks, ancestry, options) {
8331
9637
  continue;
8332
9638
  }
8333
9639
  seen.add(actionName);
8334
- if (pattern.routeFor(info.qualifiedName, actionName) !== null) {
9640
+ if (pattern.routesFor(info.qualifiedName, actionName).length > 0) {
8335
9641
  actions.push(action);
8336
9642
  }
8337
9643
  }
@@ -8356,16 +9662,52 @@ function publicInstanceMethods(blocks, bodyBlocks) {
8356
9662
  }
8357
9663
  return found;
8358
9664
  }
8359
- function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
9665
+ function respondingHelpersOf(ancestry, pattern, options) {
9666
+ const methods = /* @__PURE__ */ new Map();
9667
+ const known = /* @__PURE__ */ new Map();
9668
+ const declared = new Set(
9669
+ (pattern.responseStatusCalls ?? []).map((call) => call.name)
9670
+ );
9671
+ const methodNamed = (name) => {
9672
+ if (!methods.has(name)) {
9673
+ const found = methodInAncestry(ancestry, name, {
9674
+ facts: options.facts,
9675
+ bodyBlocks: options.bodyBlocks,
9676
+ dynamicNames: options.dynamicNames
9677
+ });
9678
+ methods.set(name, found.type === "found" ? found.method : null);
9679
+ }
9680
+ return methods.get(name) ?? null;
9681
+ };
9682
+ return (name, call) => {
9683
+ const method = declared.has(name) ? null : methodNamed(name);
9684
+ if (method === null) {
9685
+ return null;
9686
+ }
9687
+ const bound = boundStatusArguments(method, call, pattern, options.facts);
9688
+ const key = `${name}(${[...bound].join(",")})`;
9689
+ if (!known.has(key)) {
9690
+ known.set(key, helperResponse(method, pattern, options.facts, bound));
9691
+ }
9692
+ return known.get(key) ?? null;
9693
+ };
9694
+ }
9695
+ function servesFormat(pattern, route) {
9696
+ return pattern.optionalFormat === true && route.formatless !== true && !route.path.endsWith("/");
9697
+ }
9698
+ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, route, method, file, bodyRead, wrappers, respondingHelper, raises2) {
8360
9699
  const range = rangeOf(method);
8361
- const route = pattern.routeFor(controllerQualifiedName, actionName);
8362
9700
  const body = bodyOfMethod(method, file.absolute, bodyRead);
8363
9701
  const perResponse = responseBranches(
8364
9702
  method,
8365
9703
  pattern,
8366
9704
  body.effects ?? [],
8367
9705
  body.extraEffects,
8368
- { facts: bodyRead.facts }
9706
+ {
9707
+ facts: bodyRead.facts,
9708
+ respondingHelper,
9709
+ raises: raises2
9710
+ }
8369
9711
  );
8370
9712
  return {
8371
9713
  identity: {
@@ -8382,7 +9724,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8382
9724
  transport: pack.protocol,
8383
9725
  method: route.method,
8384
9726
  path: route.path,
8385
- recognition: pack.name
9727
+ recognition: pack.name,
9728
+ optionalFormat: servesFormat(pattern, route)
8386
9729
  }),
8387
9730
  parameters: [],
8388
9731
  branches: withSlotSources(
@@ -8402,7 +9745,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8402
9745
  location: range
8403
9746
  },
8404
9747
  statusCodeReading: {
8405
- reading: absentReading2,
9748
+ reading: absentReading3,
8406
9749
  libraryDefault: pattern.defaultStatusCode
8407
9750
  },
8408
9751
  effects: body.effects ?? [],
@@ -8412,6 +9755,14 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
8412
9755
  }
8413
9756
  ],
8414
9757
  body.provenance
9758
+ ).concat(
9759
+ raisedStatusBranches(
9760
+ pattern,
9761
+ bodyRead.storage?.patterns ?? [],
9762
+ body.extraEffects,
9763
+ raises2.handlers,
9764
+ range
9765
+ )
8415
9766
  ),
8416
9767
  bodyContent: body.bodyContent ?? "absent",
8417
9768
  dependencyCalls: [],
@@ -9241,6 +10592,7 @@ import {
9241
10592
  placeArgTargets as placeArgTargets2,
9242
10593
  placeCalleeParameters as placeCalleeParameters2,
9243
10594
  placeCalls as placeCalls2,
10595
+ placePossibleCalls as placePossibleCalls2,
9244
10596
  placeReceiverClasses,
9245
10597
  recordParameterGaps as recordParameterGaps2,
9246
10598
  summaryIdFromParts,
@@ -9252,11 +10604,13 @@ import {
9252
10604
  composeWrappers,
9253
10605
  createCacheLayer,
9254
10606
  createTimer,
10607
+ extractionCacheDirFor,
9255
10608
  extractionConfigStamp,
9256
10609
  KeptParses,
9257
10610
  matchesTestFileName as matchesTestFileName3,
9258
10611
  moduleInitStructure,
9259
10612
  noopTimer,
10613
+ ownersWhereWritten,
9260
10614
  runDigest,
9261
10615
  stampModuleImports,
9262
10616
  stampModules
@@ -9311,7 +10665,7 @@ var EXIT_METHODS = /* @__PURE__ */ new Map([
9311
10665
  var DEFERRED_BODY_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
9312
10666
  function exitSites(root) {
9313
10667
  const found = [];
9314
- walkDescendants4(root, null, {
10668
+ walkDescendants4(root, null, children, {
9315
10669
  at: (node) => {
9316
10670
  const site = node.type === "call" ? exitSiteAt(node) : null;
9317
10671
  if (site !== null) {
@@ -9428,7 +10782,7 @@ function eventsOf(args, called, callbacks, db) {
9428
10782
  import path3 from "path";
9429
10783
  import { SKIP_CHILDREN as SKIP_CHILDREN5, walkDescendants as walkDescendants5 } from "@suss/extractor";
9430
10784
  function emitRequireFacts(db, filePath, root, known) {
9431
- walkDescendants5(root, null, {
10785
+ walkDescendants5(root, null, children, {
9432
10786
  at: (node) => {
9433
10787
  const target = requiredRelativePath(node);
9434
10788
  if (target === null) {
@@ -9467,8 +10821,10 @@ import {
9467
10821
  placeArgTargets,
9468
10822
  placeCalleeParameters,
9469
10823
  placeCalls,
10824
+ placePossibleCalls,
9470
10825
  ReceiverClasses,
9471
10826
  recordParameterGaps,
10827
+ StopList,
9472
10828
  TargetPlacements,
9473
10829
  unfollowedCallGap,
9474
10830
  worthRecording
@@ -9510,7 +10866,7 @@ function optionParserFlagReads(method, parameterNames3) {
9510
10866
  return reads;
9511
10867
  }
9512
10868
  function walkBody2(body, at) {
9513
- walkDescendants6(body, null, {
10869
+ walkDescendants6(body, null, children, {
9514
10870
  at,
9515
10871
  into: (node) => DEFERRED_BODY_TYPES4.has(node.type) ? SKIP_CHILDREN6 : null
9516
10872
  });
@@ -9526,7 +10882,7 @@ function flagsDeclaredIn(construction) {
9526
10882
  return [];
9527
10883
  }
9528
10884
  const flags = [];
9529
- walkDescendants6(block, null, {
10885
+ walkDescendants6(block, null, children, {
9530
10886
  at: (node) => {
9531
10887
  if (node.type !== "call" || field(node, "method")?.text !== "on") {
9532
10888
  return;
@@ -9541,8 +10897,8 @@ function flagsDeclaredIn(construction) {
9541
10897
  return flags;
9542
10898
  }
9543
10899
  function flagOf(args) {
9544
- const written = args.map((arg) => stringLiteralValue(arg)).filter((text2) => text2 !== null && text2.startsWith("-"));
9545
- const chosen = written.find((text2) => text2.startsWith("--")) ?? written[0] ?? null;
10900
+ const written = args.map((arg) => stringLiteralValue(arg)).filter((text3) => text3 !== null && text3.startsWith("-"));
10901
+ const chosen = written.find((text3) => text3.startsWith("--")) ?? written[0] ?? null;
9546
10902
  if (chosen === null) {
9547
10903
  return null;
9548
10904
  }
@@ -9584,10 +10940,9 @@ import {
9584
10940
  calleeOutcomeOf,
9585
10941
  calleeOutcomes,
9586
10942
  classMemberName as classMemberName3,
9587
- couldBeSettled
10943
+ couldBeSettled,
10944
+ dispatchOutcomes
9588
10945
  } from "@suss/resolution";
9589
- var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
9590
- var INVOKES_RECEIVER = "call";
9591
10946
  var STOP_FOR = {
9592
10947
  severalSources: "multipleSources",
9593
10948
  outsideRun: "outsideRun",
@@ -9604,6 +10959,11 @@ var UNSETTLED_ON_SELF = {
9604
10959
  reason: "noDeclaration",
9605
10960
  matchByName: true
9606
10961
  };
10962
+ var SELF_CALL_IN_MIXIN = {
10963
+ kind: "stopped",
10964
+ reason: "selfCallInMixin",
10965
+ matchByName: true
10966
+ };
9607
10967
  function followed(target) {
9608
10968
  return { kind: "followed", target };
9609
10969
  }
@@ -9665,6 +11025,57 @@ function loaderSourceCallee(call, site, ctx) {
9665
11025
  }
9666
11026
  return null;
9667
11027
  }
11028
+ function dispatchedCalls(calls, ctx) {
11029
+ const byKey = new Map(
11030
+ calls.map((one) => [nodeId(one.site.file, one.call), one])
11031
+ );
11032
+ const found = /* @__PURE__ */ new Map();
11033
+ const outcomes = dispatchOutcomes(ctx.facts, [...byKey.keys()], RUBY_PROGRAM);
11034
+ for (const [key, outcome] of outcomes) {
11035
+ const asked = byKey.get(key);
11036
+ if (asked === void 0) {
11037
+ continue;
11038
+ }
11039
+ const fits = sendFits(asked.call, asked.site, ctx);
11040
+ const targets = /* @__PURE__ */ new Map();
11041
+ for (const fn of outcome.functions) {
11042
+ for (const target of reachedThrough(fn, outcome.through, ctx)) {
11043
+ if (fits(target)) {
11044
+ targets.set(nodeId(target.file, target.node), target);
11045
+ }
11046
+ }
11047
+ }
11048
+ found.set(asked.call.id, {
11049
+ targets: [...targets.values()],
11050
+ certain: outcome.certain && outcome.functions.every((fn) => ctx.definitions.has(fn)),
11051
+ unresolved: outcome.unresolved
11052
+ });
11053
+ }
11054
+ return found;
11055
+ }
11056
+ function reachedThrough(key, through, ctx) {
11057
+ const own = ctx.definitions.get(key);
11058
+ if (own !== void 0) {
11059
+ return [own];
11060
+ }
11061
+ return (through.get(key) ?? []).flatMap((inner) => {
11062
+ const target = ctx.definitions.get(inner);
11063
+ return target === void 0 ? [] : [target];
11064
+ });
11065
+ }
11066
+ function sendFits(call, site, ctx) {
11067
+ const method = calleeMethodName(call);
11068
+ if (method === void 0 || !SENDING_METHODS.has(method)) {
11069
+ return () => true;
11070
+ }
11071
+ const receiver = field(call, "receiver");
11072
+ const toClass = receiver === null || receiver.type === "self" ? inClassMethod(site, ctx) : readsOffClassItself(
11073
+ receiver,
11074
+ readKey(site.file, receiver, site.method),
11075
+ classLocalsFor(call, site)
11076
+ );
11077
+ return (target) => definesClassMethod(target.node, ctx.bodyBlocks) === toClass;
11078
+ }
9668
11079
  function outcomeFor(key, ctx, read) {
9669
11080
  return read?.outcomes.get(key) ?? calleeOutcomeOf(ctx.facts, key, RUBY_PROGRAM);
9670
11081
  }
@@ -9678,14 +11089,16 @@ function readsAsACall(call, site, ctx, read) {
9678
11089
  return false;
9679
11090
  }
9680
11091
  const outcome = outcomeFor(spelling.key, ctx, read);
9681
- return outcome.kind === "function" || outcome.kind === "object";
11092
+ return outcome.kind === "function" || outcome.kind === "object" || objectsBehind(ctx.facts, spelling.key).some(
11093
+ (key) => ctx.classNames.has(key)
11094
+ );
9682
11095
  }
9683
11096
  function spellingFor2(call, site) {
9684
11097
  const methodName = calleeMethodName(call);
9685
11098
  if (methodName === void 0) {
9686
11099
  return { kind: "stopped", reason: "noDeclaration" };
9687
11100
  }
9688
- if (DYNAMIC_SEND_NAMES.has(methodName)) {
11101
+ if (SENDING_METHODS.has(methodName)) {
9689
11102
  return { kind: "stopped", reason: "unsettledValue" };
9690
11103
  }
9691
11104
  const receiver = field(call, "receiver");
@@ -9731,7 +11144,7 @@ function asCallee(spelling, outcome, site, ctx) {
9731
11144
  return methodOnObject(spelling, objectKey, ctx);
9732
11145
  }
9733
11146
  if (outcome.kind === "function") {
9734
- return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
11147
+ return spelling.method === INVOKE_METHOD ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
9735
11148
  }
9736
11149
  return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
9737
11150
  }
@@ -9775,7 +11188,7 @@ function scopeOn(qualifiedName, method, ctx) {
9775
11188
  return null;
9776
11189
  }
9777
11190
  function fromCaller(spelling, parameterKey, site) {
9778
- if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKES_RECEIVER) {
11191
+ if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKE_METHOD) {
9779
11192
  return stop("unsettledValue");
9780
11193
  }
9781
11194
  return stop("callerSupplied");
@@ -9792,22 +11205,27 @@ function leavesRoomForATopLevelMethod(reason) {
9792
11205
  return reason === "noDeclaration" || reason === "outsideRun";
9793
11206
  }
9794
11207
  function resolveImplicitSelf(methodName, site, ctx) {
9795
- if (site.enclosingQualifiedName !== null) {
9796
- const onClass = methodOnSelf(
9797
- site,
9798
- site.enclosingQualifiedName,
9799
- methodName,
9800
- ctx
9801
- );
9802
- if (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason)) {
9803
- return onClass;
9804
- }
11208
+ const onClass = site.enclosingQualifiedName === null ? null : methodOnSelf(site, site.enclosingQualifiedName, methodName, ctx);
11209
+ if (onClass !== null && (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason))) {
11210
+ return onClass;
9805
11211
  }
11212
+ const readWholeAncestry = onClass?.kind === "stopped" && onClass.reason === "noDeclaration";
9806
11213
  const topLevel = resolveTopLevelName(methodName, ctx);
9807
11214
  if (topLevel.kind === "followed" || topLevel.reason !== "noDeclaration") {
9808
11215
  return topLevel;
9809
11216
  }
9810
- return declaredForTheOtherSelf(site, methodName, ctx) ? NO_DECLARATION : UNSETTLED_ON_SELF;
11217
+ if (declaredForTheOtherSelf(site, methodName, ctx)) {
11218
+ return NO_DECLARATION;
11219
+ }
11220
+ return readWholeAncestry && !OBJECT_METHODS.has(methodName) && inModuleInstanceMethod(site, ctx) ? SELF_CALL_IN_MIXIN : UNSETTLED_ON_SELF;
11221
+ }
11222
+ function inModuleInstanceMethod(site, ctx) {
11223
+ const qualifiedName = site.enclosingQualifiedName;
11224
+ if (qualifiedName === null || site.method === null || inClassMethod(site, ctx)) {
11225
+ return false;
11226
+ }
11227
+ const own = ctx.ancestries.get(qualifiedName)?.[0];
11228
+ return own?.type === "bodies" && own.blocks.every((block) => block.info.kind === "module");
9811
11229
  }
9812
11230
  function declaredForTheOtherSelf(site, methodName, ctx) {
9813
11231
  const qualifiedName = site.enclosingQualifiedName;
@@ -9850,12 +11268,16 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
9850
11268
  return followed(reachedMethod(found.method, found.block, methodName));
9851
11269
  }
9852
11270
  if (found.type === "unsettled") {
9853
- return stop(
9854
- found.cause === "dynamicDefine" ? "definedAtLoadTime" : "outsideRun"
9855
- );
11271
+ return stop(unsettledReason(found));
9856
11272
  }
9857
11273
  return NO_DECLARATION;
9858
11274
  }
11275
+ function unsettledReason(found) {
11276
+ if (found.cause === "dynamicDefine") {
11277
+ return "definedAtLoadTime";
11278
+ }
11279
+ return found.definedLater === true ? "behindUnreadAncestor" : "outsideRun";
11280
+ }
9859
11281
  function reachedMethod(node, block, name) {
9860
11282
  return {
9861
11283
  file: block.file,
@@ -9902,6 +11324,7 @@ async function reachedFunctions(seeds, options) {
9902
11324
  const targetsByKey = /* @__PURE__ */ new Map();
9903
11325
  const stopsByKey = /* @__PURE__ */ new Map();
9904
11326
  const argTargetsByKey = /* @__PURE__ */ new Map();
11327
+ const possibleTargetsByKey = /* @__PURE__ */ new Map();
9905
11328
  const parameterCallsByKey = /* @__PURE__ */ new Map();
9906
11329
  const propertyReadsByKey = /* @__PURE__ */ new Map();
9907
11330
  const receiverClassesByKey = /* @__PURE__ */ new Map();
@@ -9960,6 +11383,7 @@ async function reachedFunctions(seeds, options) {
9960
11383
  }
9961
11384
  targetsByKey.set(key, scan.targets);
9962
11385
  argTargetsByKey.set(key, scan.argTargets);
11386
+ possibleTargetsByKey.set(key, scan.possibleTargets);
9963
11387
  if (scan.propertyReads.size > 0) {
9964
11388
  propertyReadsByKey.set(key, scan.propertyReads);
9965
11389
  }
@@ -10023,6 +11447,7 @@ async function reachedFunctions(seeds, options) {
10023
11447
  }
10024
11448
  placeCalls(summary2, targetsByKey.get(key));
10025
11449
  placeArgTargets(summary2, argTargetsByKey.get(key));
11450
+ placePossibleCalls(summary2, possibleTargetsByKey.get(key));
10026
11451
  placeCalleeParameters(summary2, parameterCallsByKey.get(key));
10027
11452
  return summary2;
10028
11453
  };
@@ -10045,6 +11470,7 @@ async function reachedFunctions(seeds, options) {
10045
11470
  targetsByKey,
10046
11471
  stopsByKey,
10047
11472
  argTargetsByKey,
11473
+ possibleTargetsByKey,
10048
11474
  parameterCallsByKey,
10049
11475
  propertyReadsByKey,
10050
11476
  receiverClassesByKey,
@@ -10074,36 +11500,70 @@ function batchedReads(round, options) {
10074
11500
  const bodies = round.flatMap(
10075
11501
  (entry) => "source" in entry ? [{ key: entry.key, ...bodyOf2(entry.source, options) }] : []
10076
11502
  );
10077
- const spellings = calleeSpellings(
10078
- bodies.flatMap((body) => body.written),
10079
- options.context
11503
+ const written = bodies.flatMap((body) => body.written);
11504
+ const spellings = calleeSpellings(written, options.context);
11505
+ const dispatched = dispatchedAmong(written, spellings, options.context);
11506
+ return new Map(
11507
+ bodies.map((body) => [body.key, { ...body, spellings, dispatched }])
11508
+ );
11509
+ }
11510
+ function dispatchedAmong(written, spellings, ctx) {
11511
+ return dispatchedCalls(
11512
+ written.filter(({ call, site }) => {
11513
+ if (!mayRunAnEntry(call)) {
11514
+ return false;
11515
+ }
11516
+ const outcome = resolveCallee(call, site, ctx, spellings);
11517
+ return outcome.kind === "stopped" && outcome.reason !== "callerSupplied";
11518
+ }),
11519
+ ctx
10080
11520
  );
10081
- return new Map(bodies.map((body) => [body.key, { ...body, spellings }]));
11521
+ }
11522
+ function mayRunAnEntry(call) {
11523
+ const sent = calleeMethodName(call);
11524
+ if (sent !== void 0 && SENDING_METHODS.has(sent)) {
11525
+ return true;
11526
+ }
11527
+ if (call.type !== "call" || field(call, "receiver") === null) {
11528
+ return false;
11529
+ }
11530
+ const method = field(call, "method");
11531
+ return method === null || method.text === INVOKE_METHOD;
10082
11532
  }
10083
11533
  function scanOnce(source, key, options, read, charges) {
10084
11534
  const ledger = options.ledger;
10085
11535
  if (ledger === void 0) {
10086
- const body = read ?? {
10087
- ...bodyOf2(source, options),
10088
- spellings: { spellingOf: /* @__PURE__ */ new Map(), outcomes: /* @__PURE__ */ new Map() }
10089
- };
10090
- return scanBody(source, options.context, options, body);
11536
+ return scanBody(
11537
+ source,
11538
+ options.context,
11539
+ options,
11540
+ read ?? ownReads(source, options)
11541
+ );
10091
11542
  }
10092
11543
  return ledger.charging(chargeOf(charges, key), () => {
10093
11544
  forgetEvaluations(options.context.facts);
10094
11545
  ledger.readFile(source.file);
10095
- const body = bodyOf2(source, options);
10096
- const spellings = calleeSpellings(body.written, options.context);
10097
- const scan = scanBody(source, options.context, options, {
10098
- ...body,
10099
- spellings
10100
- });
11546
+ const scan = scanBody(
11547
+ source,
11548
+ options.context,
11549
+ options,
11550
+ ownReads(source, options)
11551
+ );
10101
11552
  for (const target of scan.followed) {
10102
11553
  ledger.touchKey(keyOf(target));
10103
11554
  }
10104
11555
  return scan;
10105
11556
  });
10106
11557
  }
11558
+ function ownReads(source, options) {
11559
+ const body = bodyOf2(source, options);
11560
+ const spellings = calleeSpellings(body.written, options.context);
11561
+ return {
11562
+ ...body,
11563
+ spellings,
11564
+ dispatched: dispatchedAmong(body.written, spellings, options.context)
11565
+ };
11566
+ }
10107
11567
  function storedScan(scan) {
10108
11568
  return {
10109
11569
  followed: scan.followed.map((target) => ({
@@ -10119,6 +11579,10 @@ function storedScan(scan) {
10119
11579
  callee,
10120
11580
  [...byPosition]
10121
11581
  ]),
11582
+ possibleTargets: [...scan.possibleTargets].map(([callee, places]) => [
11583
+ callee,
11584
+ [...places]
11585
+ ]),
10122
11586
  parameterCalls: [...scan.parameterCalls],
10123
11587
  passedPositions: [...scan.passedPositions],
10124
11588
  propertyReads: [...scan.propertyReads],
@@ -10136,6 +11600,7 @@ function scanFromStored(stored, functionFor) {
10136
11600
  new Map(byPosition)
10137
11601
  ])
10138
11602
  ),
11603
+ possibleTargets: new Map(stored.possibleTargets),
10139
11604
  parameterCalls: stored.parameterCalls,
10140
11605
  passedPositions: new Set(stored.passedPositions),
10141
11606
  propertyReads: new Set(stored.propertyReads),
@@ -10164,6 +11629,7 @@ var EMPTY_SCAN = {
10164
11629
  stops: [],
10165
11630
  targets: /* @__PURE__ */ new Map(),
10166
11631
  argTargets: /* @__PURE__ */ new Map(),
11632
+ possibleTargets: /* @__PURE__ */ new Map(),
10167
11633
  parameterCalls: [],
10168
11634
  passedPositions: /* @__PURE__ */ new Set(),
10169
11635
  propertyReads: /* @__PURE__ */ new Set(),
@@ -10185,7 +11651,7 @@ function bodyOf2(source, options) {
10185
11651
  const file = run?.fileOf(call) ?? source.file;
10186
11652
  return file === source.file ? site : { ...site, file, method: enclosingDefinition(call) };
10187
11653
  };
10188
- const written = callsWrittenIn(source.node, run, options);
11654
+ const written = callsWrittenIn2(source.node, run, options);
10189
11655
  const calls = callsReported(
10190
11656
  written,
10191
11657
  (call) => mightReadAsACall(call, siteFor(call), options.context)
@@ -10199,7 +11665,7 @@ function bodyOf2(source, options) {
10199
11665
  written: calls.map((call) => ({ call, site: siteFor(call) }))
10200
11666
  };
10201
11667
  }
10202
- function callsWrittenIn(node, run, options) {
11668
+ function callsWrittenIn2(node, run, options) {
10203
11669
  if (run !== null) {
10204
11670
  return exampleCalls(run, options.inheritedMethods).filter(
10205
11671
  (call) => !run.isRunnerCall(call)
@@ -10251,7 +11717,7 @@ function scanBody(source, ctx, options, read) {
10251
11717
  );
10252
11718
  const ownParameters = positionalParameters(source.node).map((p) => p.name);
10253
11719
  const followed2 = [];
10254
- const stops = [];
11720
+ const stops = new StopList();
10255
11721
  const placements = new TargetPlacements();
10256
11722
  const receiverClasses = new ReceiverClasses();
10257
11723
  const parameterCalls = [];
@@ -10266,6 +11732,12 @@ function scanBody(source, ctx, options, read) {
10266
11732
  }
10267
11733
  const argNodes = bodyStatements(args);
10268
11734
  for (let position = 0; position < argNodes.length; position += 1) {
11735
+ if (argNodes[position]?.type === LAMBDA_TYPE) {
11736
+ if (calleeKey !== null) {
11737
+ passedPositions.add(`${calleeKey}#${position}`);
11738
+ }
11739
+ continue;
11740
+ }
10269
11741
  const symbol = methodReferenceSymbol(argNodes[position]);
10270
11742
  if (symbol === null) {
10271
11743
  continue;
@@ -10303,11 +11775,35 @@ function scanBody(source, ctx, options, read) {
10303
11775
  span: spanOf(target.node)
10304
11776
  });
10305
11777
  };
11778
+ const follow = (target) => {
11779
+ const key = keyOf(target);
11780
+ if (!seen.has(key)) {
11781
+ seen.add(key);
11782
+ followed2.push(target);
11783
+ }
11784
+ };
10306
11785
  for (const call of calls) {
10307
11786
  const callee = calleeText(call);
10308
11787
  const callSite = read.siteFor(call);
10309
11788
  const predicate = read.predicateOf(call);
10310
- const outcome = predicate === null ? resolveCallee(call, callSite, ctx, read.spellings) : resolveMethodOn(predicate.target, predicate.method, callSite, ctx);
11789
+ const settled = predicate === null ? resolveCallee(call, callSite, ctx, read.spellings) : resolveMethodOn(predicate.target, predicate.method, callSite, ctx);
11790
+ const dispatched = settled.kind === "stopped" ? read.dispatched.get(call.id) : void 0;
11791
+ const [only] = dispatched?.targets ?? [];
11792
+ if (dispatched !== void 0 && only !== void 0 && !dispatched.certain) {
11793
+ dispatched.targets.forEach(follow);
11794
+ placements.placePossible(
11795
+ callee,
11796
+ dispatched.targets.map((target) => ({
11797
+ file: displayPathOf(target.file),
11798
+ span: spanOf(target.node)
11799
+ }))
11800
+ );
11801
+ if (dispatched.unresolved > 0) {
11802
+ stops.add({ callee, reason: "unsettledValue" });
11803
+ }
11804
+ continue;
11805
+ }
11806
+ const outcome = only === void 0 ? settled : { kind: "followed", target: only };
10311
11807
  if (options.storage !== void 0) {
10312
11808
  for (const callback of callbacksReached(
10313
11809
  call,
@@ -10335,11 +11831,13 @@ function scanBody(source, ctx, options, read) {
10335
11831
  outcome.kind === "followed" ? keyOf(outcome.target) : null
10336
11832
  );
10337
11833
  if (outcome.kind === "stopped") {
10338
- const stopKey = `${outcome.reason}:${callee}`;
10339
11834
  const claimed = options.storage !== void 0 && storageClaims(call, callSite.file, options.storage, callSite.method);
10340
- if (!seen.has(stopKey) && !claimed && worthRecording(outcome.reason)) {
10341
- seen.add(stopKey);
10342
- stops.push({ callee, reason: outcome.reason });
11835
+ if (!claimed && worthRecording(outcome.reason)) {
11836
+ stops.add({
11837
+ callee,
11838
+ reason: outcome.reason,
11839
+ ...resultUsedAsValue(call) ? { usedAsValue: true } : {}
11840
+ });
10343
11841
  }
10344
11842
  if (outcome.reason === "callerSupplied" && !parameterCallsSeen.has(callee)) {
10345
11843
  const receiverName = field(call, "receiver")?.text;
@@ -10354,17 +11852,14 @@ function scanBody(source, ctx, options, read) {
10354
11852
  if (isArglessReceiverCall(call)) {
10355
11853
  keptArgless.add(call.id);
10356
11854
  }
10357
- const key = keyOf(outcome.target);
10358
- if (!seen.has(key)) {
10359
- seen.add(key);
10360
- followed2.push(outcome.target);
10361
- }
11855
+ follow(outcome.target);
10362
11856
  }
10363
11857
  return {
10364
11858
  followed: followed2,
10365
- stops,
11859
+ stops: stops.stops,
10366
11860
  targets: placements.targets,
10367
11861
  argTargets: placements.argTargets,
11862
+ possibleTargets: placements.possibleTargets,
10368
11863
  parameterCalls,
10369
11864
  passedPositions,
10370
11865
  propertyReads: propertyReadsAmong(read.argless, calls, keptArgless),
@@ -10396,9 +11891,9 @@ function propertyReadsAmong(argless, made, keptArgless) {
10396
11891
  );
10397
11892
  const reads = /* @__PURE__ */ new Set();
10398
11893
  for (const call of argless) {
10399
- const text2 = calleeText(call);
10400
- if (!kept.has(text2)) {
10401
- reads.add(text2);
11894
+ const text3 = calleeText(call);
11895
+ if (!kept.has(text3)) {
11896
+ reads.add(text3);
10402
11897
  }
10403
11898
  }
10404
11899
  return reads;
@@ -10836,7 +12331,7 @@ import { literalOf as literalOf10 } from "@suss/values";
10836
12331
  function factoriesIn(packs) {
10837
12332
  return packs.flatMap((pack) => pack.factories ?? []);
10838
12333
  }
10839
- function emitFactoryFacts(db, files, isTestFile, patterns, inflections) {
12334
+ function emitFactoryFacts(db, files, isTestFile2, patterns, inflections) {
10840
12335
  for (const pattern of patterns) {
10841
12336
  const definitions = /* @__PURE__ */ new Map();
10842
12337
  for (const { file, root } of files) {
@@ -10844,7 +12339,7 @@ function emitFactoryFacts(db, files, isTestFile, patterns, inflections) {
10844
12339
  }
10845
12340
  const classes = new FactoryClasses(db, definitions, inflections);
10846
12341
  for (const { file, root } of files) {
10847
- if (!isTestFile(file)) {
12342
+ if (!isTestFile2(file)) {
10848
12343
  continue;
10849
12344
  }
10850
12345
  for (const [call, name] of buildsIn(db, root, pattern)) {
@@ -11338,7 +12833,7 @@ function replaysCleanly(units, discovered) {
11338
12833
  async function extractRubyProject(options) {
11339
12834
  const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
11340
12835
  const cacheDir = adapterStamp.declineWhenRunFromSource(
11341
- options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? path4.join(options.projectRoot, ".suss", "cache") : null)
12836
+ options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? extractionCacheDirFor(options.projectRoot) : null)
11342
12837
  );
11343
12838
  const layer = createCacheLayer(cacheDir);
11344
12839
  const packsDigest = `${adapterStamp.packsDigest(
@@ -11359,10 +12854,13 @@ async function extractRubyProject(options) {
11359
12854
  "cache.lookup",
11360
12855
  () => layer.lookup(input)
11361
12856
  );
12857
+ const composed = (stored) => composeWrappers(stored, {
12858
+ gapHandling: options.gapHandling ?? "permissive"
12859
+ });
11362
12860
  if (lookup.kind === "hit") {
11363
12861
  options.onCacheDiagnostic?.(lookup.diagnostic);
11364
12862
  options.onTiming?.(timer.report());
11365
- return { summaries: lookup.summaries, facts: new Database2() };
12863
+ return { summaries: composed(lookup.summaries), facts: new Database2() };
11366
12864
  }
11367
12865
  const plan = lookup.diagnostic.missReason === "files-changed" ? await timer.timeAsync("cache.plan", () => layer.plan(input)) : null;
11368
12866
  if (plan !== null && plan.changed.size === 0 && plan.removed.size === 0) {
@@ -11372,7 +12870,7 @@ async function extractRubyProject(options) {
11372
12870
  }
11373
12871
  options.onCacheDiagnostic?.({ kind: "hit" });
11374
12872
  options.onTiming?.(timer.report());
11375
- return { summaries: plan.allSummaries(), facts: new Database2() };
12873
+ return { summaries: composed(plan.allSummaries()), facts: new Database2() };
11376
12874
  }
11377
12875
  const cache = { timer, cacheDir, layer, input };
11378
12876
  const records = plan === null ? null : recordsToReplay(plan.roots, plan.changed, plan.removed);
@@ -11733,6 +13231,7 @@ async function runRuby(options, cache, previous) {
11733
13231
  }
11734
13232
  placeCalls2(summary, reached.targetsByKey.get(key));
11735
13233
  placeArgTargets2(summary, reached.argTargetsByKey.get(key));
13234
+ placePossibleCalls2(summary, reached.possibleTargetsByKey.get(key));
11736
13235
  placeCalleeParameters2(summary, reached.parameterCallsByKey.get(key));
11737
13236
  placeReceiverClasses(summary, reached.receiverClassesByKey.get(key));
11738
13237
  }
@@ -11779,16 +13278,20 @@ async function runRuby(options, cache, previous) {
11779
13278
  );
11780
13279
  return composeWrappers(summaries, { gapHandling });
11781
13280
  });
13281
+ const walkedFiles = new Map(
13282
+ parsed.map(({ file }) => [displayPathOf(file), file])
13283
+ );
11782
13284
  await timer.timeAsync("cache.write", async () => {
11783
- if (cacheDir === null || composed.length === 0) {
13285
+ if (cacheDir === null || summaries.length === 0) {
11784
13286
  return;
11785
13287
  }
11786
13288
  try {
11787
13289
  await cache.layer.write(
11788
13290
  cache.input,
11789
- composed,
13291
+ summaries,
11790
13292
  log === null || ledger === null || overBudget ? void 0 : rubyAttribution({
11791
- composed,
13293
+ summaries,
13294
+ walkedFileOf: (locationFile) => walkedFiles.get(locationFile),
11792
13295
  discoveries,
11793
13296
  reached,
11794
13297
  reuse,
@@ -11863,7 +13366,10 @@ function rubyAttribution(args) {
11863
13366
  facts: args.facts.get(file) ?? { own: "", joined: "" }
11864
13367
  }
11865
13368
  }));
11866
- return { roots, owners: args.composed.map(() => []) };
13369
+ return {
13370
+ roots,
13371
+ owners: ownersWhereWritten(args.summaries, args.walkedFileOf)
13372
+ };
11867
13373
  }
11868
13374
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
11869
13375
  function findRubyFiles(root) {
@@ -11937,8 +13443,8 @@ function displayNameOf(node) {
11937
13443
  if (node.type === "class" || node.type === "module") {
11938
13444
  return field(node, "name")?.text ?? node.text;
11939
13445
  }
11940
- const text2 = node.text.split("\n")[0].trim();
11941
- return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
13446
+ const text3 = node.text.split("\n")[0].trim();
13447
+ return text3.length > 40 ? `${text3.slice(0, 40)}...` : text3;
11942
13448
  }
11943
13449
  function width(node) {
11944
13450
  return node.endIndex - node.startIndex;
@@ -11968,14 +13474,14 @@ var RubyWhySession = class {
11968
13474
  * The smallest expression on `line` of `file` whose text is exactly
11969
13475
  * `text`, or null.
11970
13476
  */
11971
- findExpression(file, line, text2) {
13477
+ findExpression(file, line, text3) {
11972
13478
  const root = this.rootOf(file);
11973
13479
  if (root === null) {
11974
13480
  return null;
11975
13481
  }
11976
13482
  let found = null;
11977
13483
  const visit = (node) => {
11978
- if (node.startPosition.row + 1 === line && node.text === text2) {
13484
+ if (node.startPosition.row + 1 === line && node.text === text3) {
11979
13485
  if (found === null || width(node) <= width(found)) {
11980
13486
  found = node;
11981
13487
  }
@@ -12080,7 +13586,7 @@ function requireTarget(node) {
12080
13586
  }
12081
13587
  function requireSitesIn(root, file) {
12082
13588
  const found = [];
12083
- walkDescendants7(root, null, {
13589
+ walkDescendants7(root, null, children, {
12084
13590
  at: (node) => {
12085
13591
  const target = requireTarget(node);
12086
13592
  if (target !== null) {
@@ -12130,6 +13636,7 @@ export {
12130
13636
  bindEvaluator,
12131
13637
  bodyStatements,
12132
13638
  booleanLiteralValue,
13639
+ children,
12133
13640
  collectFileConstants,
12134
13641
  createConstantFileCache,
12135
13642
  createFileCache,