@suss/adapter-ruby 0.34.1 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -271,8 +271,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
271
271
  "protected",
272
272
  "public"
273
273
  ]);
274
- function visibilityKeyword(text2) {
275
- return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
274
+ function visibilityKeyword(text3) {
275
+ return VISIBILITY_KEYWORDS.has(text3) ? text3 : null;
276
276
  }
277
277
  function setVisibility(visibility, name, keyword) {
278
278
  if (keyword === "public") {
@@ -542,8 +542,8 @@ function lowerAcronyms(word, acronyms) {
542
542
  (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
543
543
  );
544
544
  }
545
- function escapeRegExp(text2) {
546
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
545
+ function escapeRegExp(text3) {
546
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
547
547
  }
548
548
  var PATH_CONVENTIONS = {
549
549
  railsUnderscore: underscoreConstantPath
@@ -606,7 +606,7 @@ function isDirectory(directory) {
606
606
  }
607
607
 
608
608
  // src/defineMethod.ts
609
- import { nodeOfKey as nodeOfKey2 } from "@suss/resolution";
609
+ import { nodeOfKey as nodeOfKey2, spanOfKey } from "@suss/resolution";
610
610
  import { hole as hole2, literalOf as literalOf3, piecesOf } from "@suss/values";
611
611
 
612
612
  // src/values/evaluator.ts
@@ -710,6 +710,15 @@ var OBJECT_METHODS = /* @__PURE__ */ new Set([
710
710
  "warn",
711
711
  "yield_self"
712
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";
713
722
  var CONSTRUCTOR = "new";
714
723
 
715
724
  // src/facts/resolve.ts
@@ -781,12 +790,16 @@ function constructionSites(db, classKey) {
781
790
 
782
791
  // src/facts/values.ts
783
792
  import {
793
+ addEntryRows,
784
794
  addPackWords,
795
+ addTemplateRows,
785
796
  classMemberName,
786
797
  NAMED_STORE_NAME,
787
798
  RECEIVER_STORE_NAME,
799
+ SPREAD_ENTRY,
788
800
  startsAtName,
789
801
  UNPLACED_STORE_NAME,
802
+ VALUE_ENTRY,
790
803
  valueLeftByWrites
791
804
  } from "@suss/resolution";
792
805
 
@@ -1319,6 +1332,9 @@ function add(emitter, relation, ...tuple) {
1319
1332
  }
1320
1333
  emitter.db.add(relation, tuple);
1321
1334
  }
1335
+ function rowsOf(emitter) {
1336
+ return (relation, row) => add(emitter, relation, ...row);
1337
+ }
1322
1338
  function valueKey(emitter, written, parent) {
1323
1339
  return readKey(emitter.filePath, written, emitter.enclosing, parent);
1324
1340
  }
@@ -1402,6 +1418,97 @@ function emitCall(emitter, site) {
1402
1418
  }
1403
1419
  emitCallArguments(emitter, site);
1404
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
+ }
1405
1512
  }
1406
1513
  function emitConversion(emitter, site) {
1407
1514
  const { method, receiver, node } = site;
@@ -1475,7 +1582,6 @@ var SELF_READS = {
1475
1582
  function classSpelling(name) {
1476
1583
  return name === CONSTRUCTOR ? name : classMemberName(name);
1477
1584
  }
1478
- var INVOKE_METHOD = "call";
1479
1585
  function invokedValueOf(call) {
1480
1586
  if (call.type !== "call") {
1481
1587
  return null;
@@ -1521,6 +1627,7 @@ function emitArray(emitter, array) {
1521
1627
  const objectKey = nodeId(emitter.filePath, array);
1522
1628
  add(emitter, "objectValue", objectKey);
1523
1629
  let position = 0;
1630
+ const entries = [];
1524
1631
  for (const element of children(array)) {
1525
1632
  add(
1526
1633
  emitter,
@@ -1530,12 +1637,24 @@ function emitArray(emitter, array) {
1530
1637
  valueKey(emitter, element, array)
1531
1638
  );
1532
1639
  position += 1;
1640
+ entries.push(writtenEntry(emitter, element, array));
1533
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 };
1534
1648
  }
1535
1649
  function emitHash(emitter, hash) {
1536
1650
  const objectKey = nodeId(emitter.filePath, hash);
1537
1651
  add(emitter, "objectValue", objectKey);
1652
+ const entries = [];
1538
1653
  for (const pair of children(hash)) {
1654
+ if (SPREAD_TYPES.has(pair.type)) {
1655
+ entries.push(writtenEntry(emitter, pair, hash));
1656
+ continue;
1657
+ }
1539
1658
  if (pair.type !== "pair") {
1540
1659
  continue;
1541
1660
  }
@@ -1544,6 +1663,7 @@ function emitHash(emitter, hash) {
1544
1663
  if (key === null || value === null) {
1545
1664
  continue;
1546
1665
  }
1666
+ entries.push(writtenEntry(emitter, value, pair));
1547
1667
  const keyText = pairKeyText(key);
1548
1668
  if (keyText === null) {
1549
1669
  add(emitter, "holdsUnderKey", objectKey, valueKey(emitter, value, pair));
@@ -1557,32 +1677,66 @@ function emitHash(emitter, hash) {
1557
1677
  valueKey(emitter, value, pair)
1558
1678
  );
1559
1679
  }
1680
+ addEntryRows(rowsOf(emitter), objectKey, entries);
1560
1681
  }
1561
1682
  var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1562
1683
  var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
1563
- var DEFINE_METHOD_CALL = "define_method";
1564
- function loopTurnAt(emitter, block, call) {
1565
- if (call.type !== "call") {
1566
- return null;
1567
- }
1568
- const receiver = field(call, "receiver");
1569
- 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");
1570
1688
  if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
1571
- return null;
1689
+ return;
1572
1690
  }
1573
- const parameters = field(block, "parameters");
1574
- const names = parameters === null ? [] : children(parameters).map((p) => p.text);
1575
- const element = names[0];
1576
- if (element === void 0) {
1577
- 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;
1698
+ }
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
+ );
1578
1718
  }
1579
- return {
1580
- element,
1581
- index: names[1] ?? "",
1582
- overKey: nodeId(emitter.filePath, receiver)
1583
- };
1584
1719
  }
1585
- function emitDynamicDefinition(emitter, call, type, turns) {
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;
1737
+ }
1738
+ var DEFINE_METHOD_CALL = "define_method";
1739
+ function emitDynamicDefinition(emitter, call, type) {
1586
1740
  if (emitter.selfKey === null || // A call inside a method runs when the method runs, not when the class loads.
1587
1741
  emitter.enclosing !== null || type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
1588
1742
  return;
@@ -1591,33 +1745,16 @@ function emitDynamicDefinition(emitter, call, type, turns) {
1591
1745
  const first = args === null ? void 0 : children(args)[0];
1592
1746
  const nameKey2 = nodeId(emitter.filePath, first ?? call);
1593
1747
  add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
1594
- for (const turn of turns) {
1595
- add(
1596
- emitter,
1597
- "nameTurnsOn",
1598
- nameKey2,
1599
- turn.element,
1600
- turn.index,
1601
- turn.overKey
1602
- );
1603
- }
1604
1748
  }
1605
- function walkExpressions(node, emitter, visit, nested = null, turns = []) {
1749
+ function walkExpressions(node, visit, nested = null) {
1606
1750
  for (const child of children(node)) {
1607
1751
  const type = child.type;
1608
1752
  if (OWN_BODY_TYPES.has(type)) {
1609
1753
  nested?.push(child);
1610
1754
  continue;
1611
1755
  }
1612
- visit(child, type, node, turns);
1613
- const opened = BLOCK_TYPES3.has(type) ? loopTurnAt(emitter, child, node) : null;
1614
- walkExpressions(
1615
- child,
1616
- emitter,
1617
- visit,
1618
- nested,
1619
- opened === null ? turns : [...turns, opened]
1620
- );
1756
+ visit(child, type, node);
1757
+ walkExpressions(child, visit, nested);
1621
1758
  }
1622
1759
  }
1623
1760
  var WRITTEN_KEY_TYPES = /* @__PURE__ */ new Set([
@@ -1631,13 +1768,16 @@ function emitKeyedRead(emitter, site, container, key, keyParent) {
1631
1768
  if (WRITTEN_KEY_TYPES.has(key.type)) {
1632
1769
  return;
1633
1770
  }
1771
+ const siteKey = nodeId(emitter.filePath, site);
1772
+ const containerKey = valueKey(emitter, container, site);
1634
1773
  add(
1635
1774
  emitter,
1636
1775
  "readsKeyed",
1637
- nodeId(emitter.filePath, site),
1638
- valueKey(emitter, container, site),
1776
+ siteKey,
1777
+ containerKey,
1639
1778
  valueKey(emitter, key, keyParent)
1640
1779
  );
1780
+ add(emitter, "choosesEntry", siteKey, containerKey);
1641
1781
  }
1642
1782
  function emitKeyedElement(emitter, node, parent) {
1643
1783
  const object = field(node, "object");
@@ -1698,14 +1838,14 @@ function raises(written, enclosing) {
1698
1838
  return RAISING_METHODS2.has(field(node, "method")?.text ?? "") && (receiver === null || receiver.text === "Kernel");
1699
1839
  }
1700
1840
  function emitExpressionFacts(emitter, node) {
1701
- emitDynamicDefinition(emitter, node, node.type, []);
1702
- walkExpressions(node, emitter, (child, type, parent, turns) => {
1841
+ emitDynamicDefinition(emitter, node, node.type);
1842
+ walkExpressions(node, (child, type, parent) => {
1703
1843
  const site = callSiteAt(emitter, child, type, parent);
1704
- emitExpressionFact(emitter, child, type, parent, turns, site);
1844
+ emitExpressionFact(emitter, child, type, parent, site);
1705
1845
  });
1706
1846
  }
1707
- function emitExpressionFact(emitter, child, type, parent, turns, site) {
1708
- emitDynamicDefinition(emitter, child, type, turns);
1847
+ function emitExpressionFact(emitter, child, type, parent, site) {
1848
+ emitDynamicDefinition(emitter, child, type);
1709
1849
  if (site !== null) {
1710
1850
  emitCall(emitter, site);
1711
1851
  emitKeyedFetch(emitter, site);
@@ -1746,6 +1886,11 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1746
1886
  }
1747
1887
  if (BLOCK_TYPES3.has(type)) {
1748
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);
1749
1894
  }
1750
1895
  for (const branch of BRANCHES_OF[type]?.(child) ?? []) {
1751
1896
  add(
@@ -1765,6 +1910,29 @@ function emitExpressionFact(emitter, child, type, parent, turns, site) {
1765
1910
  emitParameterDefault(emitter, child);
1766
1911
  }
1767
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
+ };
1768
1936
  function emitBlockArgument(emitter, argument) {
1769
1937
  const [handed] = children(argument);
1770
1938
  if (handed !== void 0) {
@@ -2187,11 +2355,10 @@ function emitMethodFacts(emitter, method) {
2187
2355
  const collecting = { ...inside, heldRows: expressionFacts };
2188
2356
  walkExpressions(
2189
2357
  body,
2190
- inside,
2191
- (child, type, parent, turns) => {
2358
+ (child, type, parent) => {
2192
2359
  const site = callSiteAt(inside, child, type, parent);
2193
2360
  emitBodyFact(bodyFacts, inside, funcKey, child, type, site);
2194
- emitExpressionFact(collecting, child, type, parent, turns, site);
2361
+ emitExpressionFact(collecting, child, type, parent, site);
2195
2362
  },
2196
2363
  nested
2197
2364
  );
@@ -3675,7 +3842,7 @@ function contextOver(db, nodes) {
3675
3842
  }
3676
3843
 
3677
3844
  // src/values/literals.ts
3678
- 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";
3679
3846
  function literalElementsIn(value) {
3680
3847
  if (value.kind !== "sequence") {
3681
3848
  return null;
@@ -3690,8 +3857,24 @@ function literalElementsIn(value) {
3690
3857
  }
3691
3858
  return elements;
3692
3859
  }
3693
- function literalElementsOf(node, db) {
3694
- 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
+ };
3695
3878
  }
3696
3879
  function namesOf(node, db) {
3697
3880
  const value = evaluatedValue(node, db);
@@ -3713,13 +3896,16 @@ function definedNamesOf(dynamic, classKey) {
3713
3896
  }
3714
3897
  function readDynamicNames(db, rootsByFile) {
3715
3898
  const byClass = /* @__PURE__ */ new Map();
3716
- 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) {
3717
3902
  const key = String(classKey);
3903
+ const around = loopsAround(loops, key, String(nameKey2));
3718
3904
  byClass.set(
3719
3905
  key,
3720
3906
  foldReading(
3721
3907
  byClass.get(key) ?? NOTHING,
3722
- readOne(db, rootsByFile, String(nameKey2))
3908
+ readOne(db, rootsByFile, String(nameKey2), around)
3723
3909
  )
3724
3910
  );
3725
3911
  }
@@ -3740,11 +3926,11 @@ function foldReading(soFar, reading) {
3740
3926
  unreadable: soFar.unreadable
3741
3927
  };
3742
3928
  }
3743
- function readOne(db, rootsByFile, nameKey2) {
3929
+ function readOne(db, rootsByFile, nameKey2, loops) {
3744
3930
  const node = nodeOfKey2(rootsByFile, nameKey2);
3745
3931
  const names = [];
3746
3932
  const patterns = [];
3747
- for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
3933
+ for (const bindings of loopTurns(db, rootsByFile, loops)) {
3748
3934
  const value = node === null ? hole2(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
3749
3935
  const name = literalOf3(value);
3750
3936
  if (name !== null) {
@@ -3777,38 +3963,105 @@ function patternOf(value) {
3777
3963
  }
3778
3964
  return readSomething ? new RegExp(`${source}$`) : null;
3779
3965
  }
3780
- function escapeForPattern(text2) {
3781
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3966
+ function escapeForPattern(text3) {
3967
+ return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3782
3968
  }
3783
- function loopTurns(db, rootsByFile, nameKey2) {
3969
+ function loopTurns(db, rootsByFile, loops) {
3784
3970
  let turns = [/* @__PURE__ */ new Map()];
3785
- 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
+ }
3786
3976
  turns = turns.flatMap(
3787
- (base) => over.elements.map((value, position) => {
3788
- const binding = new Map([...base, [over.element, value]]);
3789
- if (over.index !== "") {
3790
- binding.set(over.index, String(position));
3791
- }
3792
- return binding;
3793
- })
3977
+ (base) => bindings.map((binding) => new Map([...base, ...binding]))
3794
3978
  );
3795
3979
  }
3796
3980
  return turns;
3797
3981
  }
3798
- function listsBehind(db, rootsByFile, nameKey2) {
3799
- const found = [];
3800
- for (const [, element, index, overKey] of db.lookup(
3801
- "nameTurnsOn",
3802
- 0,
3803
- nameKey2
3804
- )) {
3805
- const node = nodeOfKey2(rootsByFile, String(overKey));
3806
- const elements = node === null ? null : literalElementsOf(node, db);
3807
- if (elements !== null) {
3808
- 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;
3809
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" });
3810
4040
  }
3811
- 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);
3812
4065
  }
3813
4066
 
3814
4067
  // src/scope.ts
@@ -4567,9 +4820,9 @@ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
4567
4820
  ]);
4568
4821
  function literalArgOf(node, facts) {
4569
4822
  const value = evaluatedValue(node, facts);
4570
- const text2 = literalOf4(value);
4571
- if (text2 !== null) {
4572
- return { kind: "string", value: text2 };
4823
+ const text3 = literalOf4(value);
4824
+ if (text3 !== null) {
4825
+ return { kind: "string", value: text3 };
4573
4826
  }
4574
4827
  const constant2 = constantOf(value);
4575
4828
  if (typeof constant2 === "number") {
@@ -4635,7 +4888,7 @@ function isCall(node, locals, inherited) {
4635
4888
  }
4636
4889
  function collectCalls(node, read, inherited, found) {
4637
4890
  for (const child of children(node)) {
4638
- if (read.stops.has(child.type)) {
4891
+ if (read.stops.has(child.type) && !isLambdaArgument(child)) {
4639
4892
  continue;
4640
4893
  }
4641
4894
  if (isCall(child, read.locals, inherited)) {
@@ -4645,6 +4898,9 @@ function collectCalls(node, read, inherited, found) {
4645
4898
  }
4646
4899
  return found;
4647
4900
  }
4901
+ function isLambdaArgument(node) {
4902
+ return node.type === LAMBDA_TYPE && node.parent?.type === "argument_list";
4903
+ }
4648
4904
  function methodBody(definitionNode) {
4649
4905
  const body = field(definitionNode, "body");
4650
4906
  if (body === null) {
@@ -7572,7 +7828,7 @@ function envSiteAt(node) {
7572
7828
 
7573
7829
  // src/filters.ts
7574
7830
  var RESCUES_NOTHING = { refs: [], someUnread: false };
7575
- function controllerFilters(pattern, ancestry, read = {}) {
7831
+ function controllerFilterChain(pattern, ancestry, read = {}) {
7576
7832
  const forms = pattern.filters ?? [];
7577
7833
  if (forms.length === 0) {
7578
7834
  return [];
@@ -7616,27 +7872,47 @@ function controllerFilters(pattern, ancestry, read = {}) {
7616
7872
  declared = applySkips(declared, skipsOf(statement, skipped, read.facts));
7617
7873
  }
7618
7874
  }
7619
- const resolved = [];
7620
- for (const declaration of declared) {
7621
- const written = writtenAt(
7875
+ const resolved = declared.map(
7876
+ (declaration) => linkFor(
7622
7877
  declaration,
7623
- filterMethod(ancestry, declaration.methodName, read)
7624
- );
7625
- if (written === null) {
7626
- continue;
7627
- }
7628
- resolved.push({
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: {
7629
7903
  filter: declaration.filter,
7630
7904
  ...written,
7631
7905
  only: declaration.only,
7632
7906
  except: declaration.except,
7633
7907
  rescues: declaration.rescues
7634
- });
7635
- }
7636
- return [
7637
- ...resolved.filter((one) => one.filter.onThrow !== true),
7638
- ...resolved.filter((one) => one.filter.onThrow === true).reverse()
7639
- ];
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;
7640
7916
  }
7641
7917
  function writtenAt(declaration, found) {
7642
7918
  if (found.type === "found") {
@@ -7687,6 +7963,13 @@ function filterReference(filter, displayPath, caught) {
7687
7963
  }
7688
7964
  };
7689
7965
  }
7966
+ function unreadFilterReference(filter, displayPath) {
7967
+ return {
7968
+ file: displayPath,
7969
+ name: filter.methodName,
7970
+ ...filter.filter.onThrow === true ? { onThrow: true } : {}
7971
+ };
7972
+ }
7690
7973
  function filterUnit(filter, pattern, displayPath, body, reading = {}) {
7691
7974
  const range = rangeOf(filter.method);
7692
7975
  const { respondingHelper, raises: raises2 } = reading;
@@ -7770,6 +8053,7 @@ function declarationsOf(statement, filter, declaredIn, facts, inlineOrdinal) {
7770
8053
  only,
7771
8054
  except: except ?? /* @__PURE__ */ new Set(),
7772
8055
  rescues: rescues2,
8056
+ declaredIn: declaredIn.file,
7773
8057
  ...inline === void 0 ? {} : { inline }
7774
8058
  }));
7775
8059
  }
@@ -8762,8 +9046,8 @@ function moduleOfConstant(node, options) {
8762
9046
  const files = [...new Set(keys.map(fileOfKey))];
8763
9047
  return files.length === 1 ? options.displayPathOf(files[0]) : null;
8764
9048
  }
8765
- function oneLine(text2) {
8766
- return text2.replace(/\s*\n\s*/g, " ").trim();
9049
+ function oneLine(text3) {
9050
+ return text3.replace(/\s*\n\s*/g, " ").trim();
8767
9051
  }
8768
9052
 
8769
9053
  // src/testClasses.ts
@@ -9205,11 +9489,12 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9205
9489
  ])) {
9206
9490
  return [];
9207
9491
  }
9208
- const filters = controllerFilters(pattern, ancestry, {
9492
+ const chain = controllerFilterChain(pattern, ancestry, {
9209
9493
  facts: options.facts,
9210
9494
  bodyBlocks: options.bodyBlocks,
9211
9495
  dynamicNames: options.dynamicNames
9212
9496
  });
9497
+ const filters = chain.flatMap((link) => "read" in link ? [link.read] : []);
9213
9498
  const units = [];
9214
9499
  const actions = controllerActions(
9215
9500
  pattern,
@@ -9240,10 +9525,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9240
9525
  return classes === void 0 ? [] : [classes];
9241
9526
  });
9242
9527
  for (const filter of filters) {
9243
- const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
9528
+ const displayPath2 = options.displayPathOf?.(filter.file) ?? filter.file;
9244
9529
  const body = bodyOfMethod(filter.method, filter.file, options);
9245
9530
  const handler = filter.filter.onThrow === true;
9246
- const raw = filterUnit(filter, pattern, displayPath, body, {
9531
+ const raw = filterUnit(filter, pattern, displayPath2, body, {
9247
9532
  facts: options.facts,
9248
9533
  respondingHelper,
9249
9534
  ...handler ? {} : {
@@ -9271,14 +9556,14 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9271
9556
  enclosingQualifiedName: filter.enclosingQualifiedName
9272
9557
  });
9273
9558
  }
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));
9274
9565
  const emitAction = async (actionName, method, block, routes) => {
9275
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
9276
- (filter) => filterReference(
9277
- filter,
9278
- options.displayPathOf?.(filter.file) ?? filter.file,
9279
- caught.get(filter)
9280
- )
9281
- );
9566
+ const around = chain.filter((link) => filterCoversAction(filterOf(link), actionName)).map((link) => referenceTo(link));
9282
9567
  const raises2 = {
9283
9568
  classes: await raisedClasses(method, block.info.bodyNesting, exceptions),
9284
9569
  handlers: handlersAround(actionName)
@@ -10307,6 +10592,7 @@ import {
10307
10592
  placeArgTargets as placeArgTargets2,
10308
10593
  placeCalleeParameters as placeCalleeParameters2,
10309
10594
  placeCalls as placeCalls2,
10595
+ placePossibleCalls as placePossibleCalls2,
10310
10596
  placeReceiverClasses,
10311
10597
  recordParameterGaps as recordParameterGaps2,
10312
10598
  summaryIdFromParts,
@@ -10318,11 +10604,13 @@ import {
10318
10604
  composeWrappers,
10319
10605
  createCacheLayer,
10320
10606
  createTimer,
10607
+ extractionCacheDirFor,
10321
10608
  extractionConfigStamp,
10322
10609
  KeptParses,
10323
10610
  matchesTestFileName as matchesTestFileName3,
10324
10611
  moduleInitStructure,
10325
10612
  noopTimer,
10613
+ ownersWhereWritten,
10326
10614
  runDigest,
10327
10615
  stampModuleImports,
10328
10616
  stampModules
@@ -10533,6 +10821,7 @@ import {
10533
10821
  placeArgTargets,
10534
10822
  placeCalleeParameters,
10535
10823
  placeCalls,
10824
+ placePossibleCalls,
10536
10825
  ReceiverClasses,
10537
10826
  recordParameterGaps,
10538
10827
  StopList,
@@ -10608,8 +10897,8 @@ function flagsDeclaredIn(construction) {
10608
10897
  return flags;
10609
10898
  }
10610
10899
  function flagOf(args) {
10611
- const written = args.map((arg) => stringLiteralValue(arg)).filter((text2) => text2 !== null && text2.startsWith("-"));
10612
- 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;
10613
10902
  if (chosen === null) {
10614
10903
  return null;
10615
10904
  }
@@ -10651,10 +10940,9 @@ import {
10651
10940
  calleeOutcomeOf,
10652
10941
  calleeOutcomes,
10653
10942
  classMemberName as classMemberName3,
10654
- couldBeSettled
10943
+ couldBeSettled,
10944
+ dispatchOutcomes
10655
10945
  } from "@suss/resolution";
10656
- var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
10657
- var INVOKES_RECEIVER = "call";
10658
10946
  var STOP_FOR = {
10659
10947
  severalSources: "multipleSources",
10660
10948
  outsideRun: "outsideRun",
@@ -10737,6 +11025,57 @@ function loaderSourceCallee(call, site, ctx) {
10737
11025
  }
10738
11026
  return null;
10739
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
+ }
10740
11079
  function outcomeFor(key, ctx, read) {
10741
11080
  return read?.outcomes.get(key) ?? calleeOutcomeOf(ctx.facts, key, RUBY_PROGRAM);
10742
11081
  }
@@ -10750,14 +11089,16 @@ function readsAsACall(call, site, ctx, read) {
10750
11089
  return false;
10751
11090
  }
10752
11091
  const outcome = outcomeFor(spelling.key, ctx, read);
10753
- 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
+ );
10754
11095
  }
10755
11096
  function spellingFor2(call, site) {
10756
11097
  const methodName = calleeMethodName(call);
10757
11098
  if (methodName === void 0) {
10758
11099
  return { kind: "stopped", reason: "noDeclaration" };
10759
11100
  }
10760
- if (DYNAMIC_SEND_NAMES.has(methodName)) {
11101
+ if (SENDING_METHODS.has(methodName)) {
10761
11102
  return { kind: "stopped", reason: "unsettledValue" };
10762
11103
  }
10763
11104
  const receiver = field(call, "receiver");
@@ -10803,7 +11144,7 @@ function asCallee(spelling, outcome, site, ctx) {
10803
11144
  return methodOnObject(spelling, objectKey, ctx);
10804
11145
  }
10805
11146
  if (outcome.kind === "function") {
10806
- return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
11147
+ return spelling.method === INVOKE_METHOD ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
10807
11148
  }
10808
11149
  return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
10809
11150
  }
@@ -10847,7 +11188,7 @@ function scopeOn(qualifiedName, method, ctx) {
10847
11188
  return null;
10848
11189
  }
10849
11190
  function fromCaller(spelling, parameterKey, site) {
10850
- if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKES_RECEIVER) {
11191
+ if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKE_METHOD) {
10851
11192
  return stop("unsettledValue");
10852
11193
  }
10853
11194
  return stop("callerSupplied");
@@ -10983,6 +11324,7 @@ async function reachedFunctions(seeds, options) {
10983
11324
  const targetsByKey = /* @__PURE__ */ new Map();
10984
11325
  const stopsByKey = /* @__PURE__ */ new Map();
10985
11326
  const argTargetsByKey = /* @__PURE__ */ new Map();
11327
+ const possibleTargetsByKey = /* @__PURE__ */ new Map();
10986
11328
  const parameterCallsByKey = /* @__PURE__ */ new Map();
10987
11329
  const propertyReadsByKey = /* @__PURE__ */ new Map();
10988
11330
  const receiverClassesByKey = /* @__PURE__ */ new Map();
@@ -11041,6 +11383,7 @@ async function reachedFunctions(seeds, options) {
11041
11383
  }
11042
11384
  targetsByKey.set(key, scan.targets);
11043
11385
  argTargetsByKey.set(key, scan.argTargets);
11386
+ possibleTargetsByKey.set(key, scan.possibleTargets);
11044
11387
  if (scan.propertyReads.size > 0) {
11045
11388
  propertyReadsByKey.set(key, scan.propertyReads);
11046
11389
  }
@@ -11104,6 +11447,7 @@ async function reachedFunctions(seeds, options) {
11104
11447
  }
11105
11448
  placeCalls(summary2, targetsByKey.get(key));
11106
11449
  placeArgTargets(summary2, argTargetsByKey.get(key));
11450
+ placePossibleCalls(summary2, possibleTargetsByKey.get(key));
11107
11451
  placeCalleeParameters(summary2, parameterCallsByKey.get(key));
11108
11452
  return summary2;
11109
11453
  };
@@ -11126,6 +11470,7 @@ async function reachedFunctions(seeds, options) {
11126
11470
  targetsByKey,
11127
11471
  stopsByKey,
11128
11472
  argTargetsByKey,
11473
+ possibleTargetsByKey,
11129
11474
  parameterCallsByKey,
11130
11475
  propertyReadsByKey,
11131
11476
  receiverClassesByKey,
@@ -11155,36 +11500,70 @@ function batchedReads(round, options) {
11155
11500
  const bodies = round.flatMap(
11156
11501
  (entry) => "source" in entry ? [{ key: entry.key, ...bodyOf2(entry.source, options) }] : []
11157
11502
  );
11158
- const spellings = calleeSpellings(
11159
- bodies.flatMap((body) => body.written),
11160
- 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
11161
11520
  );
11162
- 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;
11163
11532
  }
11164
11533
  function scanOnce(source, key, options, read, charges) {
11165
11534
  const ledger = options.ledger;
11166
11535
  if (ledger === void 0) {
11167
- const body = read ?? {
11168
- ...bodyOf2(source, options),
11169
- spellings: { spellingOf: /* @__PURE__ */ new Map(), outcomes: /* @__PURE__ */ new Map() }
11170
- };
11171
- return scanBody(source, options.context, options, body);
11536
+ return scanBody(
11537
+ source,
11538
+ options.context,
11539
+ options,
11540
+ read ?? ownReads(source, options)
11541
+ );
11172
11542
  }
11173
11543
  return ledger.charging(chargeOf(charges, key), () => {
11174
11544
  forgetEvaluations(options.context.facts);
11175
11545
  ledger.readFile(source.file);
11176
- const body = bodyOf2(source, options);
11177
- const spellings = calleeSpellings(body.written, options.context);
11178
- const scan = scanBody(source, options.context, options, {
11179
- ...body,
11180
- spellings
11181
- });
11546
+ const scan = scanBody(
11547
+ source,
11548
+ options.context,
11549
+ options,
11550
+ ownReads(source, options)
11551
+ );
11182
11552
  for (const target of scan.followed) {
11183
11553
  ledger.touchKey(keyOf(target));
11184
11554
  }
11185
11555
  return scan;
11186
11556
  });
11187
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
+ }
11188
11567
  function storedScan(scan) {
11189
11568
  return {
11190
11569
  followed: scan.followed.map((target) => ({
@@ -11200,6 +11579,10 @@ function storedScan(scan) {
11200
11579
  callee,
11201
11580
  [...byPosition]
11202
11581
  ]),
11582
+ possibleTargets: [...scan.possibleTargets].map(([callee, places]) => [
11583
+ callee,
11584
+ [...places]
11585
+ ]),
11203
11586
  parameterCalls: [...scan.parameterCalls],
11204
11587
  passedPositions: [...scan.passedPositions],
11205
11588
  propertyReads: [...scan.propertyReads],
@@ -11217,6 +11600,7 @@ function scanFromStored(stored, functionFor) {
11217
11600
  new Map(byPosition)
11218
11601
  ])
11219
11602
  ),
11603
+ possibleTargets: new Map(stored.possibleTargets),
11220
11604
  parameterCalls: stored.parameterCalls,
11221
11605
  passedPositions: new Set(stored.passedPositions),
11222
11606
  propertyReads: new Set(stored.propertyReads),
@@ -11245,6 +11629,7 @@ var EMPTY_SCAN = {
11245
11629
  stops: [],
11246
11630
  targets: /* @__PURE__ */ new Map(),
11247
11631
  argTargets: /* @__PURE__ */ new Map(),
11632
+ possibleTargets: /* @__PURE__ */ new Map(),
11248
11633
  parameterCalls: [],
11249
11634
  passedPositions: /* @__PURE__ */ new Set(),
11250
11635
  propertyReads: /* @__PURE__ */ new Set(),
@@ -11266,7 +11651,7 @@ function bodyOf2(source, options) {
11266
11651
  const file = run?.fileOf(call) ?? source.file;
11267
11652
  return file === source.file ? site : { ...site, file, method: enclosingDefinition(call) };
11268
11653
  };
11269
- const written = callsWrittenIn(source.node, run, options);
11654
+ const written = callsWrittenIn2(source.node, run, options);
11270
11655
  const calls = callsReported(
11271
11656
  written,
11272
11657
  (call) => mightReadAsACall(call, siteFor(call), options.context)
@@ -11280,7 +11665,7 @@ function bodyOf2(source, options) {
11280
11665
  written: calls.map((call) => ({ call, site: siteFor(call) }))
11281
11666
  };
11282
11667
  }
11283
- function callsWrittenIn(node, run, options) {
11668
+ function callsWrittenIn2(node, run, options) {
11284
11669
  if (run !== null) {
11285
11670
  return exampleCalls(run, options.inheritedMethods).filter(
11286
11671
  (call) => !run.isRunnerCall(call)
@@ -11347,6 +11732,12 @@ function scanBody(source, ctx, options, read) {
11347
11732
  }
11348
11733
  const argNodes = bodyStatements(args);
11349
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
+ }
11350
11741
  const symbol = methodReferenceSymbol(argNodes[position]);
11351
11742
  if (symbol === null) {
11352
11743
  continue;
@@ -11384,11 +11775,35 @@ function scanBody(source, ctx, options, read) {
11384
11775
  span: spanOf(target.node)
11385
11776
  });
11386
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
+ };
11387
11785
  for (const call of calls) {
11388
11786
  const callee = calleeText(call);
11389
11787
  const callSite = read.siteFor(call);
11390
11788
  const predicate = read.predicateOf(call);
11391
- 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 };
11392
11807
  if (options.storage !== void 0) {
11393
11808
  for (const callback of callbacksReached(
11394
11809
  call,
@@ -11437,17 +11852,14 @@ function scanBody(source, ctx, options, read) {
11437
11852
  if (isArglessReceiverCall(call)) {
11438
11853
  keptArgless.add(call.id);
11439
11854
  }
11440
- const key = keyOf(outcome.target);
11441
- if (!seen.has(key)) {
11442
- seen.add(key);
11443
- followed2.push(outcome.target);
11444
- }
11855
+ follow(outcome.target);
11445
11856
  }
11446
11857
  return {
11447
11858
  followed: followed2,
11448
11859
  stops: stops.stops,
11449
11860
  targets: placements.targets,
11450
11861
  argTargets: placements.argTargets,
11862
+ possibleTargets: placements.possibleTargets,
11451
11863
  parameterCalls,
11452
11864
  passedPositions,
11453
11865
  propertyReads: propertyReadsAmong(read.argless, calls, keptArgless),
@@ -11479,9 +11891,9 @@ function propertyReadsAmong(argless, made, keptArgless) {
11479
11891
  );
11480
11892
  const reads = /* @__PURE__ */ new Set();
11481
11893
  for (const call of argless) {
11482
- const text2 = calleeText(call);
11483
- if (!kept.has(text2)) {
11484
- reads.add(text2);
11894
+ const text3 = calleeText(call);
11895
+ if (!kept.has(text3)) {
11896
+ reads.add(text3);
11485
11897
  }
11486
11898
  }
11487
11899
  return reads;
@@ -12421,7 +12833,7 @@ function replaysCleanly(units, discovered) {
12421
12833
  async function extractRubyProject(options) {
12422
12834
  const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
12423
12835
  const cacheDir = adapterStamp.declineWhenRunFromSource(
12424
- 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)
12425
12837
  );
12426
12838
  const layer = createCacheLayer(cacheDir);
12427
12839
  const packsDigest = `${adapterStamp.packsDigest(
@@ -12442,10 +12854,13 @@ async function extractRubyProject(options) {
12442
12854
  "cache.lookup",
12443
12855
  () => layer.lookup(input)
12444
12856
  );
12857
+ const composed = (stored) => composeWrappers(stored, {
12858
+ gapHandling: options.gapHandling ?? "permissive"
12859
+ });
12445
12860
  if (lookup.kind === "hit") {
12446
12861
  options.onCacheDiagnostic?.(lookup.diagnostic);
12447
12862
  options.onTiming?.(timer.report());
12448
- return { summaries: lookup.summaries, facts: new Database2() };
12863
+ return { summaries: composed(lookup.summaries), facts: new Database2() };
12449
12864
  }
12450
12865
  const plan = lookup.diagnostic.missReason === "files-changed" ? await timer.timeAsync("cache.plan", () => layer.plan(input)) : null;
12451
12866
  if (plan !== null && plan.changed.size === 0 && plan.removed.size === 0) {
@@ -12455,7 +12870,7 @@ async function extractRubyProject(options) {
12455
12870
  }
12456
12871
  options.onCacheDiagnostic?.({ kind: "hit" });
12457
12872
  options.onTiming?.(timer.report());
12458
- return { summaries: plan.allSummaries(), facts: new Database2() };
12873
+ return { summaries: composed(plan.allSummaries()), facts: new Database2() };
12459
12874
  }
12460
12875
  const cache = { timer, cacheDir, layer, input };
12461
12876
  const records = plan === null ? null : recordsToReplay(plan.roots, plan.changed, plan.removed);
@@ -12816,6 +13231,7 @@ async function runRuby(options, cache, previous) {
12816
13231
  }
12817
13232
  placeCalls2(summary, reached.targetsByKey.get(key));
12818
13233
  placeArgTargets2(summary, reached.argTargetsByKey.get(key));
13234
+ placePossibleCalls2(summary, reached.possibleTargetsByKey.get(key));
12819
13235
  placeCalleeParameters2(summary, reached.parameterCallsByKey.get(key));
12820
13236
  placeReceiverClasses(summary, reached.receiverClassesByKey.get(key));
12821
13237
  }
@@ -12862,16 +13278,20 @@ async function runRuby(options, cache, previous) {
12862
13278
  );
12863
13279
  return composeWrappers(summaries, { gapHandling });
12864
13280
  });
13281
+ const walkedFiles = new Map(
13282
+ parsed.map(({ file }) => [displayPathOf(file), file])
13283
+ );
12865
13284
  await timer.timeAsync("cache.write", async () => {
12866
- if (cacheDir === null || composed.length === 0) {
13285
+ if (cacheDir === null || summaries.length === 0) {
12867
13286
  return;
12868
13287
  }
12869
13288
  try {
12870
13289
  await cache.layer.write(
12871
13290
  cache.input,
12872
- composed,
13291
+ summaries,
12873
13292
  log === null || ledger === null || overBudget ? void 0 : rubyAttribution({
12874
- composed,
13293
+ summaries,
13294
+ walkedFileOf: (locationFile) => walkedFiles.get(locationFile),
12875
13295
  discoveries,
12876
13296
  reached,
12877
13297
  reuse,
@@ -12946,7 +13366,10 @@ function rubyAttribution(args) {
12946
13366
  facts: args.facts.get(file) ?? { own: "", joined: "" }
12947
13367
  }
12948
13368
  }));
12949
- return { roots, owners: args.composed.map(() => []) };
13369
+ return {
13370
+ roots,
13371
+ owners: ownersWhereWritten(args.summaries, args.walkedFileOf)
13372
+ };
12950
13373
  }
12951
13374
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["vendor", "node_modules", "tmp", ".git"]);
12952
13375
  function findRubyFiles(root) {
@@ -13020,8 +13443,8 @@ function displayNameOf(node) {
13020
13443
  if (node.type === "class" || node.type === "module") {
13021
13444
  return field(node, "name")?.text ?? node.text;
13022
13445
  }
13023
- const text2 = node.text.split("\n")[0].trim();
13024
- 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;
13025
13448
  }
13026
13449
  function width(node) {
13027
13450
  return node.endIndex - node.startIndex;
@@ -13051,14 +13474,14 @@ var RubyWhySession = class {
13051
13474
  * The smallest expression on `line` of `file` whose text is exactly
13052
13475
  * `text`, or null.
13053
13476
  */
13054
- findExpression(file, line, text2) {
13477
+ findExpression(file, line, text3) {
13055
13478
  const root = this.rootOf(file);
13056
13479
  if (root === null) {
13057
13480
  return null;
13058
13481
  }
13059
13482
  let found = null;
13060
13483
  const visit = (node) => {
13061
- if (node.startPosition.row + 1 === line && node.text === text2) {
13484
+ if (node.startPosition.row + 1 === line && node.text === text3) {
13062
13485
  if (found === null || width(node) <= width(found)) {
13063
13486
  found = node;
13064
13487
  }