@suss/adapter-ruby 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -350,8 +350,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
350
350
  "protected",
351
351
  "public"
352
352
  ]);
353
- function visibilityKeyword(text2) {
354
- return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
353
+ function visibilityKeyword(text3) {
354
+ return VISIBILITY_KEYWORDS.has(text3) ? text3 : null;
355
355
  }
356
356
  function setVisibility(visibility, name, keyword) {
357
357
  if (keyword === "public") {
@@ -621,8 +621,8 @@ function lowerAcronyms(word, acronyms) {
621
621
  (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
622
622
  );
623
623
  }
624
- function escapeRegExp(text2) {
625
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
624
+ function escapeRegExp(text3) {
625
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
626
626
  }
627
627
  var PATH_CONVENTIONS = {
628
628
  railsUnderscore: underscoreConstantPath
@@ -775,6 +775,15 @@ var OBJECT_METHODS = /* @__PURE__ */ new Set([
775
775
  "warn",
776
776
  "yield_self"
777
777
  ]);
778
+ var SENDING_METHODS = /* @__PURE__ */ new Set([
779
+ "send",
780
+ "public_send",
781
+ "__send__"
782
+ ]);
783
+ var METHOD_REFERENCE = "method";
784
+ var INVOKE_METHOD = "call";
785
+ var BLOCK_WRAPPERS = /* @__PURE__ */ new Set(["proc", "lambda"]);
786
+ var PROC_CLASS = "Proc";
778
787
  var CONSTRUCTOR = "new";
779
788
 
780
789
  // src/facts/resolve.ts
@@ -1376,6 +1385,9 @@ function add(emitter, relation, ...tuple) {
1376
1385
  }
1377
1386
  emitter.db.add(relation, tuple);
1378
1387
  }
1388
+ function rowsOf(emitter) {
1389
+ return (relation, row) => add(emitter, relation, ...row);
1390
+ }
1379
1391
  function valueKey(emitter, written, parent) {
1380
1392
  return readKey(emitter.filePath, written, emitter.enclosing, parent);
1381
1393
  }
@@ -1459,6 +1471,97 @@ function emitCall(emitter, site) {
1459
1471
  }
1460
1472
  emitCallArguments(emitter, site);
1461
1473
  emitConversion(emitter, site);
1474
+ emitSentChoice(emitter, site);
1475
+ emitMethodReference(emitter, site);
1476
+ emitBlockAsProc(emitter, site);
1477
+ }
1478
+ function emitBlockAsProc(emitter, site) {
1479
+ const { method, receiver, node } = site;
1480
+ const name = method?.text;
1481
+ const wraps = receiver === null ? name !== void 0 && BLOCK_WRAPPERS.has(name) : name === CONSTRUCTOR && receiver.text === PROC_CLASS;
1482
+ const block = field(node, "block");
1483
+ if (!wraps || block === null) {
1484
+ return;
1485
+ }
1486
+ const blockKey = nodeId(emitter.filePath, block);
1487
+ add(emitter, "func", blockKey);
1488
+ add(emitter, "binds", site.key, blockKey);
1489
+ for (const call of callsWrittenIn(block)) {
1490
+ add(emitter, "makesCall", blockKey, nodeId(emitter.filePath, call));
1491
+ }
1492
+ }
1493
+ function callsWrittenIn(node, found = []) {
1494
+ for (const child of children(node)) {
1495
+ if (OWN_BODY_TYPES.has(child.type)) {
1496
+ continue;
1497
+ }
1498
+ if (child.type === "call") {
1499
+ found.push(child);
1500
+ }
1501
+ callsWrittenIn(child, found);
1502
+ }
1503
+ return found;
1504
+ }
1505
+ function emitSentChoice(emitter, site) {
1506
+ const { method, receiver, node } = site;
1507
+ if (method === null || !SENDING_METHODS.has(method.text)) {
1508
+ return;
1509
+ }
1510
+ const objectKey = receiver === null ? emitter.selfKey : valueKey(emitter, receiver, node);
1511
+ const [name] = readCallArgs(field(node, "arguments")).positional;
1512
+ if (objectKey === null || name === void 0) {
1513
+ return;
1514
+ }
1515
+ add(emitter, "callsChosenEntry", site.key);
1516
+ const written = symbolValue(name) ?? stringLiteralValue(name);
1517
+ if (written !== null) {
1518
+ for (const spelled of namesReadAs(emitter, receiver, objectKey, written)) {
1519
+ add(emitter, "choosesNamedEntry", site.key, objectKey, spelled);
1520
+ }
1521
+ return;
1522
+ }
1523
+ const ends = writtenEnds(name);
1524
+ if (ends === null) {
1525
+ add(emitter, "choosesEntry", site.key, objectKey);
1526
+ return;
1527
+ }
1528
+ for (const start of namesReadAs(emitter, receiver, objectKey, ends.start)) {
1529
+ add(emitter, "choosesEntryLike", site.key, objectKey, start, ends.end);
1530
+ }
1531
+ }
1532
+ function writtenEnds(node) {
1533
+ if (node.type !== "string" && node.type !== "delimited_symbol") {
1534
+ return null;
1535
+ }
1536
+ const parts = children(node);
1537
+ const written = (part) => part.type === "string_content";
1538
+ const firstComputed = parts.findIndex((part) => !written(part));
1539
+ if (firstComputed === -1) {
1540
+ return null;
1541
+ }
1542
+ const lastComputed = parts.length - 1 - [...parts].reverse().findIndex((part) => !written(part));
1543
+ const start = parts.slice(0, firstComputed).map((part) => part.text).join("");
1544
+ const end = parts.slice(lastComputed + 1).map((part) => part.text).join("");
1545
+ return start === "" && end === "" ? null : { start, end };
1546
+ }
1547
+ function emitMethodReference(emitter, site) {
1548
+ const { method, receiver, node } = site;
1549
+ if (method?.text !== METHOD_REFERENCE || receiver !== null || emitter.selfKey === null) {
1550
+ return;
1551
+ }
1552
+ const [name] = readCallArgs(field(node, "arguments")).positional;
1553
+ const written = name === void 0 ? null : symbolValue(name) ?? stringLiteralValue(name);
1554
+ if (written === null) {
1555
+ return;
1556
+ }
1557
+ for (const spelled of namesReadAs(
1558
+ emitter,
1559
+ receiver,
1560
+ emitter.selfKey,
1561
+ written
1562
+ )) {
1563
+ add(emitter, "readsProperty", site.key, emitter.selfKey, spelled);
1564
+ }
1462
1565
  }
1463
1566
  function emitConversion(emitter, site) {
1464
1567
  const { method, receiver, node } = site;
@@ -1532,7 +1635,6 @@ var SELF_READS = {
1532
1635
  function classSpelling(name) {
1533
1636
  return name === CONSTRUCTOR ? name : (0, import_resolution4.classMemberName)(name);
1534
1637
  }
1535
- var INVOKE_METHOD = "call";
1536
1638
  function invokedValueOf(call) {
1537
1639
  if (call.type !== "call") {
1538
1640
  return null;
@@ -1578,6 +1680,7 @@ function emitArray(emitter, array) {
1578
1680
  const objectKey = nodeId(emitter.filePath, array);
1579
1681
  add(emitter, "objectValue", objectKey);
1580
1682
  let position = 0;
1683
+ const entries = [];
1581
1684
  for (const element of children(array)) {
1582
1685
  add(
1583
1686
  emitter,
@@ -1587,12 +1690,24 @@ function emitArray(emitter, array) {
1587
1690
  valueKey(emitter, element, array)
1588
1691
  );
1589
1692
  position += 1;
1693
+ entries.push(writtenEntry(emitter, element, array));
1590
1694
  }
1695
+ (0, import_resolution4.addEntryRows)(rowsOf(emitter), objectKey, entries);
1696
+ }
1697
+ var SPREAD_TYPES = /* @__PURE__ */ new Set(["splat_argument", "hash_splat_argument"]);
1698
+ function writtenEntry(emitter, entry, parent) {
1699
+ const unpacked = SPREAD_TYPES.has(entry.type) ? children(entry)[0] : void 0;
1700
+ return unpacked === void 0 ? { key: valueKey(emitter, entry, parent), form: import_resolution4.VALUE_ENTRY } : { key: valueKey(emitter, unpacked, entry), form: import_resolution4.SPREAD_ENTRY };
1591
1701
  }
1592
1702
  function emitHash(emitter, hash) {
1593
1703
  const objectKey = nodeId(emitter.filePath, hash);
1594
1704
  add(emitter, "objectValue", objectKey);
1705
+ const entries = [];
1595
1706
  for (const pair of children(hash)) {
1707
+ if (SPREAD_TYPES.has(pair.type)) {
1708
+ entries.push(writtenEntry(emitter, pair, hash));
1709
+ continue;
1710
+ }
1596
1711
  if (pair.type !== "pair") {
1597
1712
  continue;
1598
1713
  }
@@ -1601,6 +1716,7 @@ function emitHash(emitter, hash) {
1601
1716
  if (key === null || value === null) {
1602
1717
  continue;
1603
1718
  }
1719
+ entries.push(writtenEntry(emitter, value, pair));
1604
1720
  const keyText = pairKeyText(key);
1605
1721
  if (keyText === null) {
1606
1722
  add(emitter, "holdsUnderKey", objectKey, valueKey(emitter, value, pair));
@@ -1614,32 +1730,66 @@ function emitHash(emitter, hash) {
1614
1730
  valueKey(emitter, value, pair)
1615
1731
  );
1616
1732
  }
1733
+ (0, import_resolution4.addEntryRows)(rowsOf(emitter), objectKey, entries);
1617
1734
  }
1618
1735
  var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1619
1736
  var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
1620
- var DEFINE_METHOD_CALL = "define_method";
1621
- function loopTurnAt(emitter, block, call) {
1622
- if (call.type !== "call") {
1623
- return null;
1624
- }
1625
- const receiver = field(call, "receiver");
1626
- const method = field(call, "method");
1737
+ var COUNTING_LOOP = "each_with_index";
1738
+ function emitLoopVariables(emitter, block, call) {
1739
+ const receiver = call.type === "call" ? field(call, "receiver") : null;
1740
+ const method = receiver === null ? null : field(call, "method");
1627
1741
  if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
1628
- return null;
1742
+ return;
1629
1743
  }
1630
- const parameters = field(block, "parameters");
1631
- const names = parameters === null ? [] : children(parameters).map((p) => p.text);
1632
- const element = names[0];
1633
- if (element === void 0) {
1634
- return null;
1744
+ const variables = children(field(block, "parameters")).filter(
1745
+ (variable) => variable.type !== "block_parameter"
1746
+ );
1747
+ const counting = method.text === COUNTING_LOOP;
1748
+ const [first, position] = variables;
1749
+ if (first === void 0 || counting && variables.length > 2) {
1750
+ return;
1751
+ }
1752
+ const taken = counting || variables.length === 1 ? elementTakenBy(first) : partsTakenBy(variables);
1753
+ const blockKey = nodeId(emitter.filePath, block);
1754
+ const over = valueKey(emitter, receiver, call);
1755
+ for (const variable of taken) {
1756
+ const key = `${blockKey}#${variable.name}`;
1757
+ if (variable.part === null) {
1758
+ add(emitter, "elementOf", key, over);
1759
+ continue;
1760
+ }
1761
+ add(emitter, "elementPartOf", key, over, variable.part);
1762
+ }
1763
+ const [anchor] = taken;
1764
+ if (counting && position?.type === "identifier" && anchor !== void 0) {
1765
+ add(
1766
+ emitter,
1767
+ "positionOf",
1768
+ `${blockKey}#${position.text}`,
1769
+ `${blockKey}#${anchor.name}`
1770
+ );
1635
1771
  }
1636
- return {
1637
- element,
1638
- index: names[1] ?? "",
1639
- overKey: nodeId(emitter.filePath, receiver)
1640
- };
1641
1772
  }
1642
- function emitDynamicDefinition(emitter, call, type, turns) {
1773
+ function elementTakenBy(variable) {
1774
+ if (variable.type === "identifier") {
1775
+ return [{ name: variable.text, part: null }];
1776
+ }
1777
+ return variable.type === "destructured_parameter" ? partsTakenBy(children(variable)) : [];
1778
+ }
1779
+ function partsTakenBy(variables) {
1780
+ const parts = [];
1781
+ for (const [position, variable] of variables.entries()) {
1782
+ if (variable.type === "splat_parameter") {
1783
+ break;
1784
+ }
1785
+ if (variable.type === "identifier") {
1786
+ parts.push({ name: variable.text, part: String(position) });
1787
+ }
1788
+ }
1789
+ return parts;
1790
+ }
1791
+ var DEFINE_METHOD_CALL = "define_method";
1792
+ function emitDynamicDefinition(emitter, call, type) {
1643
1793
  if (emitter.selfKey === null || // A call inside a method runs when the method runs, not when the class loads.
1644
1794
  emitter.enclosing !== null || type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
1645
1795
  return;
@@ -1648,33 +1798,16 @@ function emitDynamicDefinition(emitter, call, type, turns) {
1648
1798
  const first = args === null ? void 0 : children(args)[0];
1649
1799
  const nameKey2 = nodeId(emitter.filePath, first ?? call);
1650
1800
  add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
1651
- for (const turn of turns) {
1652
- add(
1653
- emitter,
1654
- "nameTurnsOn",
1655
- nameKey2,
1656
- turn.element,
1657
- turn.index,
1658
- turn.overKey
1659
- );
1660
- }
1661
1801
  }
1662
- function walkExpressions(node, emitter, visit, nested = null, turns = []) {
1802
+ function walkExpressions(node, visit, nested = null) {
1663
1803
  for (const child of children(node)) {
1664
1804
  const type = child.type;
1665
1805
  if (OWN_BODY_TYPES.has(type)) {
1666
1806
  nested?.push(child);
1667
1807
  continue;
1668
1808
  }
1669
- visit(child, type, node, turns);
1670
- const opened = BLOCK_TYPES3.has(type) ? loopTurnAt(emitter, child, node) : null;
1671
- walkExpressions(
1672
- child,
1673
- emitter,
1674
- visit,
1675
- nested,
1676
- opened === null ? turns : [...turns, opened]
1677
- );
1809
+ visit(child, type, node);
1810
+ walkExpressions(child, visit, nested);
1678
1811
  }
1679
1812
  }
1680
1813
  var WRITTEN_KEY_TYPES = /* @__PURE__ */ new Set([
@@ -1688,13 +1821,16 @@ function emitKeyedRead(emitter, site, container, key, keyParent) {
1688
1821
  if (WRITTEN_KEY_TYPES.has(key.type)) {
1689
1822
  return;
1690
1823
  }
1824
+ const siteKey = nodeId(emitter.filePath, site);
1825
+ const containerKey = valueKey(emitter, container, site);
1691
1826
  add(
1692
1827
  emitter,
1693
1828
  "readsKeyed",
1694
- nodeId(emitter.filePath, site),
1695
- valueKey(emitter, container, site),
1829
+ siteKey,
1830
+ containerKey,
1696
1831
  valueKey(emitter, key, keyParent)
1697
1832
  );
1833
+ add(emitter, "choosesEntry", siteKey, containerKey);
1698
1834
  }
1699
1835
  function emitKeyedElement(emitter, node, parent) {
1700
1836
  const object = field(node, "object");
@@ -1755,14 +1891,14 @@ function raises(written, enclosing) {
1755
1891
  return RAISING_METHODS2.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1756
1892
  }
1757
1893
  function emitExpressionFacts(emitter, node) {
1758
- emitDynamicDefinition(emitter, node, node.type, []);
1759
- walkExpressions(node, emitter, (child, type, parent, turns) => {
1894
+ emitDynamicDefinition(emitter, node, node.type);
1895
+ walkExpressions(node, (child, type, parent) => {
1760
1896
  const site = callSiteAt(emitter, child, type, parent);
1761
- emitExpressionFact(emitter, child, type, parent, turns, site);
1897
+ emitExpressionFact(emitter, child, type, parent, site);
1762
1898
  });
1763
1899
  }
1764
- function emitExpressionFact(emitter, child, type, parent, turns, site) {
1765
- emitDynamicDefinition(emitter, child, type, turns);
1900
+ function emitExpressionFact(emitter, child, type, parent, site) {
1901
+ emitDynamicDefinition(emitter, child, type);
1766
1902
  if (site !== null) {
1767
1903
  emitCall(emitter, site);
1768
1904
  emitKeyedFetch(emitter, site);
@@ -1803,6 +1939,11 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1803
1939
  }
1804
1940
  if (BLOCK_TYPES3.has(type)) {
1805
1941
  emitBlockValue(emitter, child);
1942
+ emitLoopVariables(emitter, child, parent);
1943
+ }
1944
+ const pieces = TEMPLATE_PIECES[type]?.(emitter, child) ?? [];
1945
+ if (pieces.length > 0) {
1946
+ (0, import_resolution4.addTemplateRows)(rowsOf(emitter), nodeId(emitter.filePath, child), pieces);
1806
1947
  }
1807
1948
  for (const branch of BRANCHES_OF[type]?.(child) ?? []) {
1808
1949
  add(
@@ -1822,6 +1963,29 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1822
1963
  emitParameterDefault(emitter, child);
1823
1964
  }
1824
1965
  }
1966
+ function interpolatedPieces(emitter, string) {
1967
+ const parts = children(string);
1968
+ if (!parts.some((part) => part.type === "interpolation")) {
1969
+ return [];
1970
+ }
1971
+ return parts.flatMap((part) => {
1972
+ if (part.type === "string_content") {
1973
+ return [{ text: part.text }];
1974
+ }
1975
+ const expression = part.type === "interpolation" ? children(part)[0] : void 0;
1976
+ return expression === void 0 ? [] : [{ hole: valueKey(emitter, expression, part) }];
1977
+ });
1978
+ }
1979
+ function adjacentPieces(emitter, joined2) {
1980
+ return children(joined2).map((part) => ({
1981
+ hole: valueKey(emitter, part, joined2)
1982
+ }));
1983
+ }
1984
+ var TEMPLATE_PIECES = {
1985
+ string: interpolatedPieces,
1986
+ delimited_symbol: interpolatedPieces,
1987
+ chained_string: adjacentPieces
1988
+ };
1825
1989
  function emitBlockArgument(emitter, argument) {
1826
1990
  const [handed] = children(argument);
1827
1991
  if (handed !== void 0) {
@@ -2244,11 +2408,10 @@ function emitMethodFacts(emitter, method) {
2244
2408
  const collecting = { ...inside, heldRows: expressionFacts };
2245
2409
  walkExpressions(
2246
2410
  body,
2247
- inside,
2248
- (child, type, parent, turns) => {
2411
+ (child, type, parent) => {
2249
2412
  const site = callSiteAt(inside, child, type, parent);
2250
2413
  emitBodyFact(bodyFacts, inside, funcKey, child, type, site);
2251
- emitExpressionFact(collecting, child, type, parent, turns, site);
2414
+ emitExpressionFact(collecting, child, type, parent, site);
2252
2415
  },
2253
2416
  nested
2254
2417
  );
@@ -3728,8 +3891,24 @@ function literalElementsIn(value) {
3728
3891
  }
3729
3892
  return elements;
3730
3893
  }
3731
- function literalElementsOf(node, db) {
3732
- return literalElementsIn(evaluatedValue(node, db));
3894
+ function loopElementsOf(node, db) {
3895
+ const value = evaluatedValue(node, db);
3896
+ if (value.kind === "sequence") {
3897
+ return value.items.map((item) => elementOfList((0, import_values4.force)(item.value)));
3898
+ }
3899
+ if (value.kind === "record" && !value.open) {
3900
+ return [...value.fields].map(([key, item]) => {
3901
+ const parts = [(0, import_values4.text)(key), (0, import_values4.force)(item.value)];
3902
+ return { whole: (0, import_values4.sequence)(parts), parts };
3903
+ });
3904
+ }
3905
+ return null;
3906
+ }
3907
+ function elementOfList(whole) {
3908
+ return {
3909
+ whole,
3910
+ parts: whole.kind === "sequence" ? whole.items.map((item) => (0, import_values4.force)(item.value)) : [whole]
3911
+ };
3733
3912
  }
3734
3913
  function namesOf(node, db) {
3735
3914
  const value = evaluatedValue(node, db);
@@ -3751,13 +3930,16 @@ function definedNamesOf(dynamic, classKey) {
3751
3930
  }
3752
3931
  function readDynamicNames(db, rootsByFile) {
3753
3932
  const byClass = /* @__PURE__ */ new Map();
3754
- for (const [classKey, nameKey2] of db.facts("definesMethodFrom")) {
3933
+ const definitions = db.facts("definesMethodFrom");
3934
+ const loops = definitions.length === 0 ? /* @__PURE__ */ new Map() : loopsByFile(db);
3935
+ for (const [classKey, nameKey2] of definitions) {
3755
3936
  const key = String(classKey);
3937
+ const around = loopsAround(loops, key, String(nameKey2));
3756
3938
  byClass.set(
3757
3939
  key,
3758
3940
  foldReading(
3759
3941
  byClass.get(key) ?? NOTHING,
3760
- readOne(db, rootsByFile, String(nameKey2))
3942
+ readOne(db, rootsByFile, String(nameKey2), around)
3761
3943
  )
3762
3944
  );
3763
3945
  }
@@ -3778,11 +3960,11 @@ function foldReading(soFar, reading) {
3778
3960
  unreadable: soFar.unreadable
3779
3961
  };
3780
3962
  }
3781
- function readOne(db, rootsByFile, nameKey2) {
3963
+ function readOne(db, rootsByFile, nameKey2, loops) {
3782
3964
  const node = (0, import_resolution6.nodeOfKey)(rootsByFile, nameKey2);
3783
3965
  const names = [];
3784
3966
  const patterns = [];
3785
- for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
3967
+ for (const bindings of loopTurns(db, rootsByFile, loops)) {
3786
3968
  const value = node === null ? (0, import_values5.hole)(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
3787
3969
  const name = (0, import_values5.literalOf)(value);
3788
3970
  if (name !== null) {
@@ -3815,38 +3997,105 @@ function patternOf(value) {
3815
3997
  }
3816
3998
  return readSomething ? new RegExp(`${source}$`) : null;
3817
3999
  }
3818
- function escapeForPattern(text2) {
3819
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4000
+ function escapeForPattern(text3) {
4001
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3820
4002
  }
3821
- function loopTurns(db, rootsByFile, nameKey2) {
4003
+ function loopTurns(db, rootsByFile, loops) {
3822
4004
  let turns = [/* @__PURE__ */ new Map()];
3823
- for (const over of listsBehind(db, rootsByFile, nameKey2)) {
4005
+ for (const loop of loops) {
4006
+ const bindings = bindingsOf(db, rootsByFile, loop);
4007
+ if (bindings === null) {
4008
+ continue;
4009
+ }
3824
4010
  turns = turns.flatMap(
3825
- (base) => over.elements.map((value, position) => {
3826
- const binding = new Map([...base, [over.element, value]]);
3827
- if (over.index !== "") {
3828
- binding.set(over.index, String(position));
3829
- }
3830
- return binding;
3831
- })
4011
+ (base) => bindings.map((binding) => new Map([...base, ...binding]))
3832
4012
  );
3833
4013
  }
3834
4014
  return turns;
3835
4015
  }
3836
- function listsBehind(db, rootsByFile, nameKey2) {
3837
- const found = [];
3838
- for (const [, element, index, overKey] of db.lookup(
3839
- "nameTurnsOn",
3840
- 0,
3841
- nameKey2
3842
- )) {
3843
- const node = (0, import_resolution6.nodeOfKey)(rootsByFile, String(overKey));
3844
- const elements = node === null ? null : literalElementsOf(node, db);
3845
- if (elements !== null) {
3846
- found.push({ element: String(element), index: String(index), elements });
4016
+ function bindingsOf(db, rootsByFile, loop) {
4017
+ const node = collectionNode(rootsByFile, loop);
4018
+ const elements = node === null ? null : loopElementsOf(node, db);
4019
+ if (elements === null) {
4020
+ return null;
4021
+ }
4022
+ const bound = [...loop.variables].flatMap(([name, takes]) => {
4023
+ const values = elements.map(
4024
+ (element, position) => GIVEN[takes.kind](element, position, takes)
4025
+ );
4026
+ return values.every((value) => value !== null) ? [{ name, values }] : [];
4027
+ });
4028
+ if (bound.length === 0) {
4029
+ return null;
4030
+ }
4031
+ return elements.map(
4032
+ (_, position) => new Map(bound.map(({ name, values }) => [name, values[position] ?? ""]))
4033
+ );
4034
+ }
4035
+ var GIVEN = {
4036
+ element: (element) => (0, import_values5.literalOf)(element.whole),
4037
+ part: (element, _, takes) => {
4038
+ const part = takes.kind === "part" ? element.parts[takes.part] : void 0;
4039
+ return part === void 0 ? null : (0, import_values5.literalOf)(part);
4040
+ },
4041
+ position: (_, position) => String(position)
4042
+ };
4043
+ function collectionNode(rootsByFile, loop) {
4044
+ const written = (0, import_resolution6.nodeOfKey)(rootsByFile, loop.over);
4045
+ if (written !== null) {
4046
+ return written;
4047
+ }
4048
+ const call = (0, import_resolution6.nodeOfKey)(rootsByFile, loop.block.key)?.parent ?? null;
4049
+ return call === null ? null : field(call, "receiver");
4050
+ }
4051
+ function blockVariableOf(key) {
4052
+ const at = key.lastIndexOf("#");
4053
+ const span = at < 0 ? null : (0, import_resolution6.spanOfKey)(key.slice(0, at));
4054
+ return span === null ? null : { block: { ...span, key: key.slice(0, at) }, name: key.slice(at + 1) };
4055
+ }
4056
+ function loopsByFile(db) {
4057
+ const blocks = /* @__PURE__ */ new Map();
4058
+ const note = (key, over, takes) => {
4059
+ const variable = blockVariableOf(String(key));
4060
+ if (variable === null) {
4061
+ return;
3847
4062
  }
4063
+ const known = blocks.get(variable.block.key);
4064
+ const loop = known ?? {
4065
+ block: variable.block,
4066
+ over: String(over),
4067
+ variables: /* @__PURE__ */ new Map()
4068
+ };
4069
+ loop.variables.set(variable.name, takes);
4070
+ blocks.set(variable.block.key, loop);
4071
+ };
4072
+ for (const [element, over] of db.facts("elementOf")) {
4073
+ note(element, over, { kind: "element" });
3848
4074
  }
3849
- return found;
4075
+ for (const [element, over, part] of db.facts("elementPartOf")) {
4076
+ note(element, over, { kind: "part", part: Number(part) });
4077
+ }
4078
+ for (const [position, element] of db.facts("positionOf")) {
4079
+ const block = blockVariableOf(String(element))?.block.key;
4080
+ if (block !== void 0 && blocks.has(block)) {
4081
+ note(position, blocks.get(block)?.over, { kind: "position" });
4082
+ }
4083
+ }
4084
+ const byFile = /* @__PURE__ */ new Map();
4085
+ for (const loop of blocks.values()) {
4086
+ byFile.set(loop.block.file, [...byFile.get(loop.block.file) ?? [], loop]);
4087
+ }
4088
+ return byFile;
4089
+ }
4090
+ function loopsAround(loops, classKey, nameKey2) {
4091
+ const cls = (0, import_resolution6.spanOfKey)(classKey);
4092
+ const name = (0, import_resolution6.spanOfKey)(nameKey2);
4093
+ if (cls === null || name === null) {
4094
+ return [];
4095
+ }
4096
+ return (loops.get(name.file) ?? []).filter(
4097
+ ({ block }) => cls.start <= block.start && block.end <= cls.end && block.start <= name.start && name.end <= block.end
4098
+ ).sort((a, b) => a.block.start - b.block.start);
3850
4099
  }
3851
4100
 
3852
4101
  // src/scope.ts
@@ -4595,9 +4844,9 @@ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
4595
4844
  ]);
4596
4845
  function literalArgOf(node, facts) {
4597
4846
  const value = evaluatedValue(node, facts);
4598
- const text2 = (0, import_values7.literalOf)(value);
4599
- if (text2 !== null) {
4600
- return { kind: "string", value: text2 };
4847
+ const text3 = (0, import_values7.literalOf)(value);
4848
+ if (text3 !== null) {
4849
+ return { kind: "string", value: text3 };
4601
4850
  }
4602
4851
  const constant2 = (0, import_values7.constantOf)(value);
4603
4852
  if (typeof constant2 === "number") {
@@ -4663,7 +4912,7 @@ function isCall(node, locals, inherited) {
4663
4912
  }
4664
4913
  function collectCalls(node, read, inherited, found) {
4665
4914
  for (const child of children(node)) {
4666
- if (read.stops.has(child.type)) {
4915
+ if (read.stops.has(child.type) && !isLambdaArgument(child)) {
4667
4916
  continue;
4668
4917
  }
4669
4918
  if (isCall(child, read.locals, inherited)) {
@@ -4673,6 +4922,9 @@ function collectCalls(node, read, inherited, found) {
4673
4922
  }
4674
4923
  return found;
4675
4924
  }
4925
+ function isLambdaArgument(node) {
4926
+ return node.type === LAMBDA_TYPE && node.parent?.type === "argument_list";
4927
+ }
4676
4928
  function methodBody(definitionNode) {
4677
4929
  const body = field(definitionNode, "body");
4678
4930
  if (body === null) {
@@ -7594,7 +7846,7 @@ function envSiteAt(node) {
7594
7846
 
7595
7847
  // src/filters.ts
7596
7848
  var RESCUES_NOTHING = { refs: [], someUnread: false };
7597
- function controllerFilters(pattern, ancestry, read = {}) {
7849
+ function controllerFilterChain(pattern, ancestry, read = {}) {
7598
7850
  const forms = pattern.filters ?? [];
7599
7851
  if (forms.length === 0) {
7600
7852
  return [];
@@ -7638,27 +7890,47 @@ function controllerFilters(pattern, ancestry, read = {}) {
7638
7890
  declared = applySkips(declared, skipsOf(statement, skipped, read.facts));
7639
7891
  }
7640
7892
  }
7641
- const resolved = [];
7642
- for (const declaration of declared) {
7643
- const written = writtenAt(
7893
+ const resolved = declared.map(
7894
+ (declaration) => linkFor(
7644
7895
  declaration,
7645
- filterMethod(ancestry, declaration.methodName, read)
7646
- );
7647
- if (written === null) {
7648
- continue;
7649
- }
7650
- resolved.push({
7896
+ writtenAt(
7897
+ declaration,
7898
+ filterMethod(ancestry, declaration.methodName, read)
7899
+ )
7900
+ )
7901
+ );
7902
+ return [
7903
+ ...resolved.filter((one) => !runsAfterARaise(one)),
7904
+ ...resolved.filter(runsAfterARaise).reverse()
7905
+ ];
7906
+ }
7907
+ function linkFor(declaration, written) {
7908
+ if (written === null) {
7909
+ return {
7910
+ unread: {
7911
+ filter: declaration.filter,
7912
+ methodName: declaration.methodName,
7913
+ declaredIn: declaration.declaredIn,
7914
+ only: declaration.only,
7915
+ except: declaration.except
7916
+ }
7917
+ };
7918
+ }
7919
+ return {
7920
+ read: {
7651
7921
  filter: declaration.filter,
7652
7922
  ...written,
7653
7923
  only: declaration.only,
7654
7924
  except: declaration.except,
7655
7925
  rescues: declaration.rescues
7656
- });
7657
- }
7658
- return [
7659
- ...resolved.filter((one) => one.filter.onThrow !== true),
7660
- ...resolved.filter((one) => one.filter.onThrow === true).reverse()
7661
- ];
7926
+ }
7927
+ };
7928
+ }
7929
+ function filterOf(link) {
7930
+ return "read" in link ? link.read : link.unread;
7931
+ }
7932
+ function runsAfterARaise(link) {
7933
+ return filterOf(link).filter.onThrow === true;
7662
7934
  }
7663
7935
  function writtenAt(declaration, found) {
7664
7936
  if (found.type === "found") {
@@ -7709,6 +7981,13 @@ function filterReference(filter, displayPath, caught) {
7709
7981
  }
7710
7982
  };
7711
7983
  }
7984
+ function unreadFilterReference(filter, displayPath) {
7985
+ return {
7986
+ file: displayPath,
7987
+ name: filter.methodName,
7988
+ ...filter.filter.onThrow === true ? { onThrow: true } : {}
7989
+ };
7990
+ }
7712
7991
  function filterUnit(filter, pattern, displayPath, body, reading = {}) {
7713
7992
  const range = rangeOf(filter.method);
7714
7993
  const { respondingHelper, raises: raises2 } = reading;
@@ -7792,6 +8071,7 @@ function declarationsOf(statement, filter, declaredIn, facts, inlineOrdinal) {
7792
8071
  only,
7793
8072
  except: except ?? /* @__PURE__ */ new Set(),
7794
8073
  rescues: rescues2,
8074
+ declaredIn: declaredIn.file,
7795
8075
  ...inline === void 0 ? {} : { inline }
7796
8076
  }));
7797
8077
  }
@@ -8784,8 +9064,8 @@ function moduleOfConstant(node, options) {
8784
9064
  const files = [...new Set(keys.map(import_resolution10.fileOfKey))];
8785
9065
  return files.length === 1 ? options.displayPathOf(files[0]) : null;
8786
9066
  }
8787
- function oneLine(text2) {
8788
- return text2.replace(/\s*\n\s*/g, " ").trim();
9067
+ function oneLine(text3) {
9068
+ return text3.replace(/\s*\n\s*/g, " ").trim();
8789
9069
  }
8790
9070
 
8791
9071
  // src/testClasses.ts
@@ -9227,11 +9507,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9227
9507
  ])) {
9228
9508
  return [];
9229
9509
  }
9230
- const filters = controllerFilters(pattern, ancestry, {
9510
+ const chain = controllerFilterChain(pattern, ancestry, {
9231
9511
  facts: options.facts,
9232
9512
  bodyBlocks: options.bodyBlocks,
9233
9513
  dynamicNames: options.dynamicNames
9234
9514
  });
9515
+ const filters = chain.flatMap((link) => "read" in link ? [link.read] : []);
9235
9516
  const units = [];
9236
9517
  const actions = controllerActions(
9237
9518
  pattern,
@@ -9262,10 +9543,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9262
9543
  return classes === void 0 ? [] : [classes];
9263
9544
  });
9264
9545
  for (const filter of filters) {
9265
- const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
9546
+ const displayPath2 = options.displayPathOf?.(filter.file) ?? filter.file;
9266
9547
  const body = bodyOfMethod(filter.method, filter.file, options);
9267
9548
  const handler = filter.filter.onThrow === true;
9268
- const raw = filterUnit(filter, pattern, displayPath, body, {
9549
+ const raw = filterUnit(filter, pattern, displayPath2, body, {
9269
9550
  facts: options.facts,
9270
9551
  respondingHelper,
9271
9552
  ...handler ? {} : {
@@ -9293,14 +9574,14 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9293
9574
  enclosingQualifiedName: filter.enclosingQualifiedName
9294
9575
  });
9295
9576
  }
9577
+ const displayPath = (file) => options.displayPathOf?.(file) ?? file;
9578
+ const referenceTo = (link) => "read" in link ? filterReference(
9579
+ link.read,
9580
+ displayPath(link.read.file),
9581
+ caught.get(link.read)
9582
+ ) : unreadFilterReference(link.unread, displayPath(link.unread.declaredIn));
9296
9583
  const emitAction = async (actionName, method, block, routes) => {
9297
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
9298
- (filter) => filterReference(
9299
- filter,
9300
- options.displayPathOf?.(filter.file) ?? filter.file,
9301
- caught.get(filter)
9302
- )
9303
- );
9584
+ const around = chain.filter((link) => filterCoversAction(filterOf(link), actionName)).map((link) => referenceTo(link));
9304
9585
  const raises2 = {
9305
9586
  classes: await raisedClasses(method, block.info.bodyNesting, exceptions),
9306
9587
  handlers: handlersAround(actionName)
@@ -10567,8 +10848,8 @@ function flagsDeclaredIn(construction) {
10567
10848
  return flags;
10568
10849
  }
10569
10850
  function flagOf(args) {
10570
- const written = args.map((arg) => stringLiteralValue(arg)).filter((text2) => text2 !== null && text2.startsWith("-"));
10571
- const chosen = written.find((text2) => text2.startsWith("--")) ?? written[0] ?? null;
10851
+ const written = args.map((arg) => stringLiteralValue(arg)).filter((text3) => text3 !== null && text3.startsWith("-"));
10852
+ const chosen = written.find((text3) => text3.startsWith("--")) ?? written[0] ?? null;
10572
10853
  if (chosen === null) {
10573
10854
  return null;
10574
10855
  }
@@ -10606,8 +10887,6 @@ function parserCalledOn(receiver, parserNamed) {
10606
10887
 
10607
10888
  // src/reach/resolveCallee.ts
10608
10889
  var import_resolution12 = require("@suss/resolution");
10609
- var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
10610
- var INVOKES_RECEIVER = "call";
10611
10890
  var STOP_FOR = {
10612
10891
  severalSources: "multipleSources",
10613
10892
  outsideRun: "outsideRun",
@@ -10690,6 +10969,57 @@ function loaderSourceCallee(call, site, ctx) {
10690
10969
  }
10691
10970
  return null;
10692
10971
  }
10972
+ function dispatchedCalls(calls, ctx) {
10973
+ const byKey = new Map(
10974
+ calls.map((one) => [nodeId(one.site.file, one.call), one])
10975
+ );
10976
+ const found = /* @__PURE__ */ new Map();
10977
+ const outcomes = (0, import_resolution12.dispatchOutcomes)(ctx.facts, [...byKey.keys()], RUBY_PROGRAM);
10978
+ for (const [key, outcome] of outcomes) {
10979
+ const asked = byKey.get(key);
10980
+ if (asked === void 0) {
10981
+ continue;
10982
+ }
10983
+ const fits = sendFits(asked.call, asked.site, ctx);
10984
+ const targets = /* @__PURE__ */ new Map();
10985
+ for (const fn of outcome.functions) {
10986
+ for (const target of reachedThrough(fn, outcome.through, ctx)) {
10987
+ if (fits(target)) {
10988
+ targets.set(nodeId(target.file, target.node), target);
10989
+ }
10990
+ }
10991
+ }
10992
+ found.set(asked.call.id, {
10993
+ targets: [...targets.values()],
10994
+ certain: outcome.certain && outcome.functions.every((fn) => ctx.definitions.has(fn)),
10995
+ unresolved: outcome.unresolved
10996
+ });
10997
+ }
10998
+ return found;
10999
+ }
11000
+ function reachedThrough(key, through, ctx) {
11001
+ const own = ctx.definitions.get(key);
11002
+ if (own !== void 0) {
11003
+ return [own];
11004
+ }
11005
+ return (through.get(key) ?? []).flatMap((inner) => {
11006
+ const target = ctx.definitions.get(inner);
11007
+ return target === void 0 ? [] : [target];
11008
+ });
11009
+ }
11010
+ function sendFits(call, site, ctx) {
11011
+ const method = calleeMethodName(call);
11012
+ if (method === void 0 || !SENDING_METHODS.has(method)) {
11013
+ return () => true;
11014
+ }
11015
+ const receiver = field(call, "receiver");
11016
+ const toClass = receiver === null || receiver.type === "self" ? inClassMethod(site, ctx) : readsOffClassItself(
11017
+ receiver,
11018
+ readKey(site.file, receiver, site.method),
11019
+ classLocalsFor(call, site)
11020
+ );
11021
+ return (target) => definesClassMethod(target.node, ctx.bodyBlocks) === toClass;
11022
+ }
10693
11023
  function outcomeFor(key, ctx, read) {
10694
11024
  return read?.outcomes.get(key) ?? (0, import_resolution12.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
10695
11025
  }
@@ -10703,14 +11033,16 @@ function readsAsACall(call, site, ctx, read) {
10703
11033
  return false;
10704
11034
  }
10705
11035
  const outcome = outcomeFor(spelling.key, ctx, read);
10706
- return outcome.kind === "function" || outcome.kind === "object";
11036
+ return outcome.kind === "function" || outcome.kind === "object" || objectsBehind(ctx.facts, spelling.key).some(
11037
+ (key) => ctx.classNames.has(key)
11038
+ );
10707
11039
  }
10708
11040
  function spellingFor2(call, site) {
10709
11041
  const methodName = calleeMethodName(call);
10710
11042
  if (methodName === void 0) {
10711
11043
  return { kind: "stopped", reason: "noDeclaration" };
10712
11044
  }
10713
- if (DYNAMIC_SEND_NAMES.has(methodName)) {
11045
+ if (SENDING_METHODS.has(methodName)) {
10714
11046
  return { kind: "stopped", reason: "unsettledValue" };
10715
11047
  }
10716
11048
  const receiver = field(call, "receiver");
@@ -10756,7 +11088,7 @@ function asCallee(spelling, outcome, site, ctx) {
10756
11088
  return methodOnObject(spelling, objectKey, ctx);
10757
11089
  }
10758
11090
  if (outcome.kind === "function") {
10759
- return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
11091
+ return spelling.method === INVOKE_METHOD ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
10760
11092
  }
10761
11093
  return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
10762
11094
  }
@@ -10800,7 +11132,7 @@ function scopeOn(qualifiedName, method, ctx) {
10800
11132
  return null;
10801
11133
  }
10802
11134
  function fromCaller(spelling, parameterKey, site) {
10803
- if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKES_RECEIVER) {
11135
+ if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKE_METHOD) {
10804
11136
  return stop("unsettledValue");
10805
11137
  }
10806
11138
  return stop("callerSupplied");
@@ -10936,6 +11268,7 @@ async function reachedFunctions(seeds, options) {
10936
11268
  const targetsByKey = /* @__PURE__ */ new Map();
10937
11269
  const stopsByKey = /* @__PURE__ */ new Map();
10938
11270
  const argTargetsByKey = /* @__PURE__ */ new Map();
11271
+ const possibleTargetsByKey = /* @__PURE__ */ new Map();
10939
11272
  const parameterCallsByKey = /* @__PURE__ */ new Map();
10940
11273
  const propertyReadsByKey = /* @__PURE__ */ new Map();
10941
11274
  const receiverClassesByKey = /* @__PURE__ */ new Map();
@@ -10994,6 +11327,7 @@ async function reachedFunctions(seeds, options) {
10994
11327
  }
10995
11328
  targetsByKey.set(key, scan.targets);
10996
11329
  argTargetsByKey.set(key, scan.argTargets);
11330
+ possibleTargetsByKey.set(key, scan.possibleTargets);
10997
11331
  if (scan.propertyReads.size > 0) {
10998
11332
  propertyReadsByKey.set(key, scan.propertyReads);
10999
11333
  }
@@ -11057,6 +11391,7 @@ async function reachedFunctions(seeds, options) {
11057
11391
  }
11058
11392
  (0, import_behavioral_ir7.placeCalls)(summary2, targetsByKey.get(key));
11059
11393
  (0, import_behavioral_ir7.placeArgTargets)(summary2, argTargetsByKey.get(key));
11394
+ (0, import_behavioral_ir7.placePossibleCalls)(summary2, possibleTargetsByKey.get(key));
11060
11395
  (0, import_behavioral_ir7.placeCalleeParameters)(summary2, parameterCallsByKey.get(key));
11061
11396
  return summary2;
11062
11397
  };
@@ -11079,6 +11414,7 @@ async function reachedFunctions(seeds, options) {
11079
11414
  targetsByKey,
11080
11415
  stopsByKey,
11081
11416
  argTargetsByKey,
11417
+ possibleTargetsByKey,
11082
11418
  parameterCallsByKey,
11083
11419
  propertyReadsByKey,
11084
11420
  receiverClassesByKey,
@@ -11108,36 +11444,70 @@ function batchedReads(round, options) {
11108
11444
  const bodies = round.flatMap(
11109
11445
  (entry) => "source" in entry ? [{ key: entry.key, ...bodyOf2(entry.source, options) }] : []
11110
11446
  );
11111
- const spellings = calleeSpellings(
11112
- bodies.flatMap((body) => body.written),
11113
- options.context
11447
+ const written = bodies.flatMap((body) => body.written);
11448
+ const spellings = calleeSpellings(written, options.context);
11449
+ const dispatched = dispatchedAmong(written, spellings, options.context);
11450
+ return new Map(
11451
+ bodies.map((body) => [body.key, { ...body, spellings, dispatched }])
11452
+ );
11453
+ }
11454
+ function dispatchedAmong(written, spellings, ctx) {
11455
+ return dispatchedCalls(
11456
+ written.filter(({ call, site }) => {
11457
+ if (!mayRunAnEntry(call)) {
11458
+ return false;
11459
+ }
11460
+ const outcome = resolveCallee(call, site, ctx, spellings);
11461
+ return outcome.kind === "stopped" && outcome.reason !== "callerSupplied";
11462
+ }),
11463
+ ctx
11114
11464
  );
11115
- return new Map(bodies.map((body) => [body.key, { ...body, spellings }]));
11465
+ }
11466
+ function mayRunAnEntry(call) {
11467
+ const sent = calleeMethodName(call);
11468
+ if (sent !== void 0 && SENDING_METHODS.has(sent)) {
11469
+ return true;
11470
+ }
11471
+ if (call.type !== "call" || field(call, "receiver") === null) {
11472
+ return false;
11473
+ }
11474
+ const method = field(call, "method");
11475
+ return method === null || method.text === INVOKE_METHOD;
11116
11476
  }
11117
11477
  function scanOnce(source, key, options, read, charges) {
11118
11478
  const ledger = options.ledger;
11119
11479
  if (ledger === void 0) {
11120
- const body = read ?? {
11121
- ...bodyOf2(source, options),
11122
- spellings: { spellingOf: /* @__PURE__ */ new Map(), outcomes: /* @__PURE__ */ new Map() }
11123
- };
11124
- return scanBody(source, options.context, options, body);
11480
+ return scanBody(
11481
+ source,
11482
+ options.context,
11483
+ options,
11484
+ read ?? ownReads(source, options)
11485
+ );
11125
11486
  }
11126
11487
  return ledger.charging(chargeOf(charges, key), () => {
11127
11488
  forgetEvaluations(options.context.facts);
11128
11489
  ledger.readFile(source.file);
11129
- const body = bodyOf2(source, options);
11130
- const spellings = calleeSpellings(body.written, options.context);
11131
- const scan = scanBody(source, options.context, options, {
11132
- ...body,
11133
- spellings
11134
- });
11490
+ const scan = scanBody(
11491
+ source,
11492
+ options.context,
11493
+ options,
11494
+ ownReads(source, options)
11495
+ );
11135
11496
  for (const target of scan.followed) {
11136
11497
  ledger.touchKey(keyOf(target));
11137
11498
  }
11138
11499
  return scan;
11139
11500
  });
11140
11501
  }
11502
+ function ownReads(source, options) {
11503
+ const body = bodyOf2(source, options);
11504
+ const spellings = calleeSpellings(body.written, options.context);
11505
+ return {
11506
+ ...body,
11507
+ spellings,
11508
+ dispatched: dispatchedAmong(body.written, spellings, options.context)
11509
+ };
11510
+ }
11141
11511
  function storedScan(scan) {
11142
11512
  return {
11143
11513
  followed: scan.followed.map((target) => ({
@@ -11153,6 +11523,10 @@ function storedScan(scan) {
11153
11523
  callee,
11154
11524
  [...byPosition]
11155
11525
  ]),
11526
+ possibleTargets: [...scan.possibleTargets].map(([callee, places]) => [
11527
+ callee,
11528
+ [...places]
11529
+ ]),
11156
11530
  parameterCalls: [...scan.parameterCalls],
11157
11531
  passedPositions: [...scan.passedPositions],
11158
11532
  propertyReads: [...scan.propertyReads],
@@ -11170,6 +11544,7 @@ function scanFromStored(stored, functionFor) {
11170
11544
  new Map(byPosition)
11171
11545
  ])
11172
11546
  ),
11547
+ possibleTargets: new Map(stored.possibleTargets),
11173
11548
  parameterCalls: stored.parameterCalls,
11174
11549
  passedPositions: new Set(stored.passedPositions),
11175
11550
  propertyReads: new Set(stored.propertyReads),
@@ -11198,6 +11573,7 @@ var EMPTY_SCAN = {
11198
11573
  stops: [],
11199
11574
  targets: /* @__PURE__ */ new Map(),
11200
11575
  argTargets: /* @__PURE__ */ new Map(),
11576
+ possibleTargets: /* @__PURE__ */ new Map(),
11201
11577
  parameterCalls: [],
11202
11578
  passedPositions: /* @__PURE__ */ new Set(),
11203
11579
  propertyReads: /* @__PURE__ */ new Set(),
@@ -11219,7 +11595,7 @@ function bodyOf2(source, options) {
11219
11595
  const file = run?.fileOf(call) ?? source.file;
11220
11596
  return file === source.file ? site : { ...site, file, method: enclosingDefinition(call) };
11221
11597
  };
11222
- const written = callsWrittenIn(source.node, run, options);
11598
+ const written = callsWrittenIn2(source.node, run, options);
11223
11599
  const calls = callsReported(
11224
11600
  written,
11225
11601
  (call) => mightReadAsACall(call, siteFor(call), options.context)
@@ -11233,7 +11609,7 @@ function bodyOf2(source, options) {
11233
11609
  written: calls.map((call) => ({ call, site: siteFor(call) }))
11234
11610
  };
11235
11611
  }
11236
- function callsWrittenIn(node, run, options) {
11612
+ function callsWrittenIn2(node, run, options) {
11237
11613
  if (run !== null) {
11238
11614
  return exampleCalls(run, options.inheritedMethods).filter(
11239
11615
  (call) => !run.isRunnerCall(call)
@@ -11300,6 +11676,12 @@ function scanBody(source, ctx, options, read) {
11300
11676
  }
11301
11677
  const argNodes = bodyStatements(args);
11302
11678
  for (let position = 0; position < argNodes.length; position += 1) {
11679
+ if (argNodes[position]?.type === LAMBDA_TYPE) {
11680
+ if (calleeKey !== null) {
11681
+ passedPositions.add(`${calleeKey}#${position}`);
11682
+ }
11683
+ continue;
11684
+ }
11303
11685
  const symbol = methodReferenceSymbol(argNodes[position]);
11304
11686
  if (symbol === null) {
11305
11687
  continue;
@@ -11337,11 +11719,35 @@ function scanBody(source, ctx, options, read) {
11337
11719
  span: spanOf(target.node)
11338
11720
  });
11339
11721
  };
11722
+ const follow = (target) => {
11723
+ const key = keyOf(target);
11724
+ if (!seen.has(key)) {
11725
+ seen.add(key);
11726
+ followed2.push(target);
11727
+ }
11728
+ };
11340
11729
  for (const call of calls) {
11341
11730
  const callee = calleeText(call);
11342
11731
  const callSite = read.siteFor(call);
11343
11732
  const predicate = read.predicateOf(call);
11344
- const outcome = predicate === null ? resolveCallee(call, callSite, ctx, read.spellings) : resolveMethodOn(predicate.target, predicate.method, callSite, ctx);
11733
+ const settled = predicate === null ? resolveCallee(call, callSite, ctx, read.spellings) : resolveMethodOn(predicate.target, predicate.method, callSite, ctx);
11734
+ const dispatched = settled.kind === "stopped" ? read.dispatched.get(call.id) : void 0;
11735
+ const [only] = dispatched?.targets ?? [];
11736
+ if (dispatched !== void 0 && only !== void 0 && !dispatched.certain) {
11737
+ dispatched.targets.forEach(follow);
11738
+ placements.placePossible(
11739
+ callee,
11740
+ dispatched.targets.map((target) => ({
11741
+ file: displayPathOf(target.file),
11742
+ span: spanOf(target.node)
11743
+ }))
11744
+ );
11745
+ if (dispatched.unresolved > 0) {
11746
+ stops.add({ callee, reason: "unsettledValue" });
11747
+ }
11748
+ continue;
11749
+ }
11750
+ const outcome = only === void 0 ? settled : { kind: "followed", target: only };
11345
11751
  if (options.storage !== void 0) {
11346
11752
  for (const callback of callbacksReached(
11347
11753
  call,
@@ -11390,17 +11796,14 @@ function scanBody(source, ctx, options, read) {
11390
11796
  if (isArglessReceiverCall(call)) {
11391
11797
  keptArgless.add(call.id);
11392
11798
  }
11393
- const key = keyOf(outcome.target);
11394
- if (!seen.has(key)) {
11395
- seen.add(key);
11396
- followed2.push(outcome.target);
11397
- }
11799
+ follow(outcome.target);
11398
11800
  }
11399
11801
  return {
11400
11802
  followed: followed2,
11401
11803
  stops: stops.stops,
11402
11804
  targets: placements.targets,
11403
11805
  argTargets: placements.argTargets,
11806
+ possibleTargets: placements.possibleTargets,
11404
11807
  parameterCalls,
11405
11808
  passedPositions,
11406
11809
  propertyReads: propertyReadsAmong(read.argless, calls, keptArgless),
@@ -11432,9 +11835,9 @@ function propertyReadsAmong(argless, made, keptArgless) {
11432
11835
  );
11433
11836
  const reads = /* @__PURE__ */ new Set();
11434
11837
  for (const call of argless) {
11435
- const text2 = calleeText(call);
11436
- if (!kept.has(text2)) {
11437
- reads.add(text2);
11838
+ const text3 = calleeText(call);
11839
+ if (!kept.has(text3)) {
11840
+ reads.add(text3);
11438
11841
  }
11439
11842
  }
11440
11843
  return reads;
@@ -12369,7 +12772,7 @@ function replaysCleanly(units, discovered) {
12369
12772
  async function extractRubyProject(options) {
12370
12773
  const timer = options.onTiming !== void 0 ? (0, import_extractor19.createTimer)() : (0, import_extractor19.noopTimer)();
12371
12774
  const cacheDir = adapterStamp.declineWhenRunFromSource(
12372
- options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? import_node_path4.default.join(options.projectRoot, ".suss", "cache") : null)
12775
+ options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? (0, import_extractor19.extractionCacheDirFor)(options.projectRoot) : null)
12373
12776
  );
12374
12777
  const layer = (0, import_extractor19.createCacheLayer)(cacheDir);
12375
12778
  const packsDigest = `${adapterStamp.packsDigest(
@@ -12390,10 +12793,13 @@ async function extractRubyProject(options) {
12390
12793
  "cache.lookup",
12391
12794
  () => layer.lookup(input)
12392
12795
  );
12796
+ const composed = (stored) => (0, import_extractor19.composeWrappers)(stored, {
12797
+ gapHandling: options.gapHandling ?? "permissive"
12798
+ });
12393
12799
  if (lookup.kind === "hit") {
12394
12800
  options.onCacheDiagnostic?.(lookup.diagnostic);
12395
12801
  options.onTiming?.(timer.report());
12396
- return { summaries: lookup.summaries, facts: new import_datalog3.Database() };
12802
+ return { summaries: composed(lookup.summaries), facts: new import_datalog3.Database() };
12397
12803
  }
12398
12804
  const plan = lookup.diagnostic.missReason === "files-changed" ? await timer.timeAsync("cache.plan", () => layer.plan(input)) : null;
12399
12805
  if (plan !== null && plan.changed.size === 0 && plan.removed.size === 0) {
@@ -12403,7 +12809,7 @@ async function extractRubyProject(options) {
12403
12809
  }
12404
12810
  options.onCacheDiagnostic?.({ kind: "hit" });
12405
12811
  options.onTiming?.(timer.report());
12406
- return { summaries: plan.allSummaries(), facts: new import_datalog3.Database() };
12812
+ return { summaries: composed(plan.allSummaries()), facts: new import_datalog3.Database() };
12407
12813
  }
12408
12814
  const cache = { timer, cacheDir, layer, input };
12409
12815
  const records = plan === null ? null : (0, import_resolution16.recordsToReplay)(plan.roots, plan.changed, plan.removed);
@@ -12764,6 +13170,7 @@ async function runRuby(options, cache, previous) {
12764
13170
  }
12765
13171
  (0, import_behavioral_ir9.placeCalls)(summary, reached.targetsByKey.get(key));
12766
13172
  (0, import_behavioral_ir9.placeArgTargets)(summary, reached.argTargetsByKey.get(key));
13173
+ (0, import_behavioral_ir9.placePossibleCalls)(summary, reached.possibleTargetsByKey.get(key));
12767
13174
  (0, import_behavioral_ir9.placeCalleeParameters)(summary, reached.parameterCallsByKey.get(key));
12768
13175
  (0, import_behavioral_ir9.placeReceiverClasses)(summary, reached.receiverClassesByKey.get(key));
12769
13176
  }
@@ -12810,16 +13217,20 @@ async function runRuby(options, cache, previous) {
12810
13217
  );
12811
13218
  return (0, import_extractor19.composeWrappers)(summaries, { gapHandling });
12812
13219
  });
13220
+ const walkedFiles = new Map(
13221
+ parsed.map(({ file }) => [displayPathOf(file), file])
13222
+ );
12813
13223
  await timer.timeAsync("cache.write", async () => {
12814
- if (cacheDir === null || composed.length === 0) {
13224
+ if (cacheDir === null || summaries.length === 0) {
12815
13225
  return;
12816
13226
  }
12817
13227
  try {
12818
13228
  await cache.layer.write(
12819
13229
  cache.input,
12820
- composed,
13230
+ summaries,
12821
13231
  log === null || ledger === null || overBudget ? void 0 : rubyAttribution({
12822
- composed,
13232
+ summaries,
13233
+ walkedFileOf: (locationFile) => walkedFiles.get(locationFile),
12823
13234
  discoveries,
12824
13235
  reached,
12825
13236
  reuse,
@@ -12894,7 +13305,10 @@ function rubyAttribution(args) {
12894
13305
  facts: args.facts.get(file) ?? { own: "", joined: "" }
12895
13306
  }
12896
13307
  }));
12897
- return { roots, owners: args.composed.map(() => []) };
13308
+ return {
13309
+ roots,
13310
+ owners: (0, import_extractor19.ownersWhereWritten)(args.summaries, args.walkedFileOf)
13311
+ };
12898
13312
  }
12899
13313
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
12900
13314
  function findRubyFiles(root) {
@@ -12968,8 +13382,8 @@ function displayNameOf(node) {
12968
13382
  if (node.type === "class" || node.type === "module") {
12969
13383
  return field(node, "name")?.text ?? node.text;
12970
13384
  }
12971
- const text2 = node.text.split("\n")[0].trim();
12972
- return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
13385
+ const text3 = node.text.split("\n")[0].trim();
13386
+ return text3.length > 40 ? `${text3.slice(0, 40)}...` : text3;
12973
13387
  }
12974
13388
  function width(node) {
12975
13389
  return node.endIndex - node.startIndex;
@@ -12999,14 +13413,14 @@ var RubyWhySession = class {
12999
13413
  * The smallest expression on `line` of `file` whose text is exactly
13000
13414
  * `text`, or null.
13001
13415
  */
13002
- findExpression(file, line, text2) {
13416
+ findExpression(file, line, text3) {
13003
13417
  const root = this.rootOf(file);
13004
13418
  if (root === null) {
13005
13419
  return null;
13006
13420
  }
13007
13421
  let found = null;
13008
13422
  const visit = (node) => {
13009
- if (node.startPosition.row + 1 === line && node.text === text2) {
13423
+ if (node.startPosition.row + 1 === line && node.text === text3) {
13010
13424
  if (found === null || width(node) <= width(found)) {
13011
13425
  found = node;
13012
13426
  }