@suss/adapter-ruby 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1280,18 +1280,10 @@ import { nodeOfKey } from "@suss/resolution";
1280
1280
  import { Evaluator, force, literalOf as literalOf2, text } from "@suss/values";
1281
1281
 
1282
1282
  // src/facts/resolve.ts
1283
+ import { constant, lit, rule, variable as v } from "@suss/datalog";
1283
1284
  import {
1284
- constant,
1285
- deriveOnDemand,
1286
- evaluate,
1287
- lit,
1288
- rule,
1289
- variable as v
1290
- } from "@suss/datalog";
1291
- import {
1292
- ANSWER_RELATIONS,
1293
- RESOLUTION_QUESTIONS,
1294
- RESOLUTION_RULES,
1285
+ askResolution,
1286
+ resolutionProgram,
1295
1287
  writtenValueOf as sharedWrittenValueOf,
1296
1288
  VALUE_STEP
1297
1289
  } from "@suss/resolution";
@@ -1309,18 +1301,9 @@ var RUBY_RULES = [
1309
1301
  ]
1310
1302
  )
1311
1303
  ];
1312
- var RESOLUTION_PROGRAM = deriveOnDemand(
1313
- [...RESOLUTION_RULES, ...RUBY_RULES, ...RESOLUTION_QUESTIONS],
1314
- ANSWER_RELATIONS
1315
- );
1304
+ var RUBY_PROGRAM = resolutionProgram(RUBY_RULES);
1316
1305
  function resolveValues(db, keys) {
1317
- if (keys.length === 0) {
1318
- return;
1319
- }
1320
- for (const key of keys) {
1321
- db.add("wanted", [key]);
1322
- }
1323
- evaluate(db, RESOLUTION_PROGRAM.rules);
1306
+ askResolution(db, keys, "wanted", RUBY_PROGRAM);
1324
1307
  }
1325
1308
  function resolvedFunctions(db, key) {
1326
1309
  return db.facts("wantedResolves").filter((row) => String(row[0]) === key).map((row) => String(row[1]));
@@ -1331,27 +1314,262 @@ function writtenValueOf(db, key) {
1331
1314
  }
1332
1315
 
1333
1316
  // src/facts/values.ts
1334
- function nodeId(filePath, node) {
1335
- return `${filePath}:${node.startIndex}-${node.endIndex}`;
1336
- }
1337
- function nameId(filePath, name) {
1338
- return `${filePath}#${name}`;
1339
- }
1317
+ import { startsAtName, valueLeftByWrites } from "@suss/resolution";
1318
+
1319
+ // src/facts/locals.ts
1320
+ import { writesRunInOrder } from "@suss/resolution";
1321
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1322
+ var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES2]);
1323
+ var RUBY_NAME_TYPES = /* @__PURE__ */ new Set([
1324
+ "identifier",
1325
+ "instance_variable"
1326
+ ]);
1327
+ var TARGET_LIST_TYPES = /* @__PURE__ */ new Set([
1328
+ "left_assignment_list",
1329
+ "destructured_left_assignment",
1330
+ "rest_assignment"
1331
+ ]);
1332
+ var WHOLE_VALUE_OPERATORS = /* @__PURE__ */ new Set([
1333
+ "||=",
1334
+ "&&="
1335
+ ]);
1340
1336
  function paramNameOf(param) {
1341
1337
  return param.type === "identifier" ? param : field(param, "name") ?? null;
1342
1338
  }
1343
- function declaredParamNames(method) {
1344
- const params = field(method, "parameters");
1345
- const declared = /* @__PURE__ */ new Set();
1346
- for (const param of params === null ? [] : children2(params)) {
1339
+ function parametersOf(owner) {
1340
+ const params = field(owner, "parameters");
1341
+ return params === null ? [] : bodyStatements(params);
1342
+ }
1343
+ function parameterNames2(owner) {
1344
+ const names = /* @__PURE__ */ new Set();
1345
+ for (const param of parametersOf(owner)) {
1346
+ const name = paramNameOf(param);
1347
+ if (name !== null) {
1348
+ names.add(name.text);
1349
+ }
1350
+ }
1351
+ return names;
1352
+ }
1353
+ function targetsOf(left) {
1354
+ if (left.type === "identifier" || left.type === "constant") {
1355
+ return [left];
1356
+ }
1357
+ if (TARGET_LIST_TYPES.has(left.type)) {
1358
+ return bodyStatements(left).flatMap(targetsOf);
1359
+ }
1360
+ return [];
1361
+ }
1362
+ function writeOf(target, value, at, fromParameter) {
1363
+ return { name: target.text, target, value, at, fromParameter };
1364
+ }
1365
+ function readAssignment(node) {
1366
+ const left = field(node, "left");
1367
+ const right = field(node, "right");
1368
+ if (left === null || right === null) {
1369
+ return [];
1370
+ }
1371
+ const value = left.type === "left_assignment_list" ? null : right;
1372
+ return targetsOf(left).map((target) => writeOf(target, value, node, false));
1373
+ }
1374
+ function readOperatorAssignment(node) {
1375
+ const left = field(node, "left");
1376
+ const right = field(node, "right");
1377
+ const operator = field(node, "operator");
1378
+ if (left === null || right === null || left.type !== "identifier") {
1379
+ return [];
1380
+ }
1381
+ const whole = operator !== null && WHOLE_VALUE_OPERATORS.has(operator.text);
1382
+ return [writeOf(left, whole ? right : null, node, false)];
1383
+ }
1384
+ function readForVariable(node) {
1385
+ const pattern = field(node, "pattern");
1386
+ if (pattern === null) {
1387
+ return [];
1388
+ }
1389
+ return targetsOf(pattern).map((target) => writeOf(target, null, node, false));
1390
+ }
1391
+ function readBlockParameters(node) {
1392
+ const found = [];
1393
+ for (const param of parametersOf(node)) {
1394
+ const name = paramNameOf(param);
1395
+ if (name !== null) {
1396
+ found.push(writeOf(name, null, param, true));
1397
+ }
1398
+ }
1399
+ return found;
1400
+ }
1401
+ var WRITE_READERS = {
1402
+ assignment: readAssignment,
1403
+ operator_assignment: readOperatorAssignment,
1404
+ for: readForVariable,
1405
+ block: readBlockParameters,
1406
+ do_block: readBlockParameters
1407
+ };
1408
+ function writesUnder(body) {
1409
+ const found = [];
1410
+ const visit = (node) => {
1411
+ for (const child of bodyStatements(node)) {
1412
+ if (OWN_BODY_TYPES.has(child.type)) {
1413
+ continue;
1414
+ }
1415
+ found.push(...WRITE_READERS[child.type]?.(child) ?? []);
1416
+ visit(child);
1417
+ }
1418
+ };
1419
+ visit(body);
1420
+ return found;
1421
+ }
1422
+ var namesByTree = /* @__PURE__ */ new WeakMap();
1423
+ function cacheFor(tree) {
1424
+ const known = namesByTree.get(tree);
1425
+ if (known !== void 0) {
1426
+ return known;
1427
+ }
1428
+ const fresh = { file: null, byMethod: new NodeMap() };
1429
+ namesByTree.set(tree, fresh);
1430
+ return fresh;
1431
+ }
1432
+ function scopeNames(parameters, body) {
1433
+ const locals = new Set(parameters);
1434
+ const blockParams = /* @__PURE__ */ new Set();
1435
+ for (const write of body === null ? [] : writesUnder(body)) {
1436
+ if (write.fromParameter) {
1437
+ blockParams.add(write.name);
1438
+ continue;
1439
+ }
1440
+ locals.add(write.name);
1441
+ }
1442
+ return { locals, blockParams };
1443
+ }
1444
+ function namesOfMethod(method) {
1445
+ const cache = cacheFor(method.tree);
1446
+ const remembered = cache.byMethod.get(method);
1447
+ if (remembered !== void 0) {
1448
+ return remembered;
1449
+ }
1450
+ const names = scopeNames(parameterNames2(method), field(method, "body"));
1451
+ cache.byMethod.set(method, names);
1452
+ return names;
1453
+ }
1454
+ function namesOfFile(tree) {
1455
+ const cache = cacheFor(tree);
1456
+ cache.file ??= scopeNames(/* @__PURE__ */ new Set(), tree.rootNode);
1457
+ return cache.file;
1458
+ }
1459
+ function ownerOfName(node, name, enclosing) {
1460
+ if (node.type === "constant") {
1461
+ return null;
1462
+ }
1463
+ const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
1464
+ if (names.blockParams.has(name)) {
1465
+ const block = blockDeclaring(node, name);
1466
+ if (block !== null) {
1467
+ return block;
1468
+ }
1469
+ }
1470
+ return enclosing !== null && names.locals.has(name) ? enclosing : null;
1471
+ }
1472
+ function blockDeclaring(node, name) {
1473
+ let current = node.parent;
1474
+ while (current !== null) {
1475
+ if (BLOCK_TYPES2.has(current.type) && parameterNames2(current).has(name)) {
1476
+ return current;
1477
+ }
1478
+ if (OWN_BODY_TYPES.has(current.type)) {
1479
+ return null;
1480
+ }
1481
+ current = current.parent;
1482
+ }
1483
+ return null;
1484
+ }
1485
+ function parameterWrites(method) {
1486
+ const found = [];
1487
+ for (const param of method === null ? [] : parametersOf(method)) {
1347
1488
  const name = paramNameOf(param);
1348
1489
  if (name !== null) {
1349
- declared.add(name.text);
1490
+ found.push(writeOf(name, name, param, true));
1350
1491
  }
1351
1492
  }
1352
- return declared;
1493
+ return found;
1494
+ }
1495
+ function collectWrites(method, body) {
1496
+ const writes = [...parameterWrites(method), ...writesUnder(body)].sort(
1497
+ (left, right) => left.target.startIndex - right.target.startIndex
1498
+ );
1499
+ const targetIds = new Set(writes.map((write) => write.target.id));
1500
+ const groups = /* @__PURE__ */ new Map();
1501
+ for (const write of writes) {
1502
+ const owner = ownerOfName(write.target, write.name, method);
1503
+ const key = `${owner === null ? "" : owner.id} ${write.name}`;
1504
+ const group = groups.get(key);
1505
+ if (group === void 0) {
1506
+ groups.set(key, {
1507
+ owner,
1508
+ name: write.name,
1509
+ writes: [write],
1510
+ ordered: false
1511
+ });
1512
+ continue;
1513
+ }
1514
+ group.writes.push(write);
1515
+ }
1516
+ const rules = nameReads(targetIds);
1517
+ for (const group of groups.values()) {
1518
+ const scope = group.owner === null ? body : field(group.owner, "body");
1519
+ if (scope === null) {
1520
+ continue;
1521
+ }
1522
+ group.ordered = writesRunInOrder(
1523
+ scope,
1524
+ group.name,
1525
+ group.writes.map((write) => orderedWrite(write, scope)),
1526
+ rules
1527
+ );
1528
+ }
1529
+ return [...groups.values()];
1530
+ }
1531
+ function orderedWrite(write, scope) {
1532
+ return {
1533
+ at: write.at,
1534
+ direct: !write.fromParameter && write.at.parent?.id === scope.id
1535
+ };
1536
+ }
1537
+ function nameReads(targetIds) {
1538
+ return {
1539
+ nameTypes: RUBY_NAME_TYPES,
1540
+ laterBodies: LATER_BODY_TYPES,
1541
+ childrenOf: bodyStatements,
1542
+ isRead: (node) => isNameRead(node, targetIds)
1543
+ };
1544
+ }
1545
+ function isNameRead(node, targetIds) {
1546
+ if (targetIds.has(node.id)) {
1547
+ return false;
1548
+ }
1549
+ const parent = node.parent;
1550
+ return !(parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null);
1551
+ }
1552
+
1553
+ // src/facts/values.ts
1554
+ function nodeId(filePath, node) {
1555
+ return `${filePath}:${node.startIndex}-${node.endIndex}`;
1556
+ }
1557
+ function nameId(filePath, name) {
1558
+ return `${filePath}#${name}`;
1559
+ }
1560
+ function readThrough(node) {
1561
+ if (node.type !== "parenthesized_statements") {
1562
+ return node;
1563
+ }
1564
+ const inner = node.namedChildren.filter((child) => child !== null);
1565
+ return inner.length === 1 ? readThrough(inner[0]) : node;
1566
+ }
1567
+ function nameKey(filePath, node, enclosing) {
1568
+ const owner = ownerOfName(node, node.text, enclosing);
1569
+ return owner === null ? nameId(filePath, node.text) : `${nodeId(filePath, owner)}#${node.text}`;
1353
1570
  }
1354
- function readKey(filePath, node, enclosing) {
1571
+ function readKey(filePath, written, enclosing) {
1572
+ const node = readThrough(written);
1355
1573
  if (node.type !== "identifier" && node.type !== "constant") {
1356
1574
  return nodeId(filePath, node);
1357
1575
  }
@@ -1359,10 +1577,7 @@ function readKey(filePath, node, enclosing) {
1359
1577
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1360
1578
  return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1361
1579
  }
1362
- if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1363
- return `${nodeId(filePath, enclosing)}#${node.text}`;
1364
- }
1365
- return nameId(filePath, node.text);
1580
+ return nameKey(filePath, node, enclosing);
1366
1581
  }
1367
1582
  var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1368
1583
  "string",
@@ -1391,15 +1606,12 @@ function children2(node) {
1391
1606
  function add(emitter, relation, ...tuple) {
1392
1607
  emitter.db.add(relation, tuple);
1393
1608
  }
1394
- function valueKey(emitter, value) {
1609
+ function valueKey(emitter, written) {
1610
+ const value = readThrough(written);
1395
1611
  if (value.type !== "identifier" && value.type !== "constant") {
1396
1612
  return nodeId(emitter.filePath, value);
1397
1613
  }
1398
- const enclosing = emitter.enclosing;
1399
- if (enclosing !== null && enclosing.params.has(value.text)) {
1400
- return `${enclosing.funcKey}#${value.text}`;
1401
- }
1402
- return nameId(emitter.filePath, value.text);
1614
+ return nameKey(emitter.filePath, value, emitter.enclosing);
1403
1615
  }
1404
1616
  function pairKeyText(key) {
1405
1617
  if (key.type === "hash_key_symbol") {
@@ -1538,8 +1750,72 @@ function emitExpressionFacts(emitter, node) {
1538
1750
  if (child.type === "nil") {
1539
1751
  add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
1540
1752
  }
1753
+ if (child.type === "self" && emitter.selfKey !== null) {
1754
+ add(emitter, "binds", nodeId(emitter.filePath, child), emitter.selfKey);
1755
+ }
1756
+ if (child.type === "instance_variable") {
1757
+ emitInstanceRead(emitter, child);
1758
+ }
1759
+ if (ASSIGNMENT_TYPES.has(child.type)) {
1760
+ collectInstanceWrite(emitter, child);
1761
+ }
1541
1762
  });
1542
1763
  }
1764
+ var ASSIGNMENT_TYPES = /* @__PURE__ */ new Set(["assignment", "operator_assignment"]);
1765
+ function isWriteTarget(node) {
1766
+ const parent = node.parent;
1767
+ return parent !== null && ASSIGNMENT_TYPES.has(parent.type) && field(parent, "left")?.id === node.id;
1768
+ }
1769
+ function emitInstanceRead(emitter, node) {
1770
+ if (emitter.selfKey === null || isWriteTarget(node)) {
1771
+ return;
1772
+ }
1773
+ add(
1774
+ emitter,
1775
+ "readsProperty",
1776
+ nodeId(emitter.filePath, node),
1777
+ emitter.selfKey,
1778
+ node.text
1779
+ );
1780
+ }
1781
+ function collectInstanceWrite(emitter, node) {
1782
+ const left = field(node, "left");
1783
+ const right = field(node, "right");
1784
+ const collector = emitter.instanceWrites;
1785
+ if (left === null || right === null || collector === null) {
1786
+ return;
1787
+ }
1788
+ if (left.type !== "instance_variable") {
1789
+ return;
1790
+ }
1791
+ const operator = field(node, "operator")?.text;
1792
+ const value = node.type === "assignment" || WHOLE_VALUE_OPERATORS.has(operator ?? "") ? right : null;
1793
+ const written = collector.get(left.text) ?? [];
1794
+ written.push(
1795
+ describeWrite(emitter, {
1796
+ name: left.text,
1797
+ target: left,
1798
+ value,
1799
+ at: node,
1800
+ fromParameter: false
1801
+ })
1802
+ );
1803
+ collector.set(left.text, written);
1804
+ }
1805
+ function emitInstanceWrites(emitter, classKey, collected) {
1806
+ for (const [name, writes] of collected) {
1807
+ const settled = valueLeftByWrites(writes, false);
1808
+ if (settled !== null) {
1809
+ add(emitter, "holdsProperty", classKey, name, settled);
1810
+ continue;
1811
+ }
1812
+ for (const write of writes) {
1813
+ if (write.value !== null && !write.narrowsName) {
1814
+ add(emitter, "holdsProperty", classKey, name, write.value);
1815
+ }
1816
+ }
1817
+ }
1818
+ }
1543
1819
  function implicitReturn(body) {
1544
1820
  const statements = children2(body).filter(
1545
1821
  (child) => child.type !== "rescue" && child.type !== "ensure"
@@ -1550,23 +1826,17 @@ function implicitReturn(body) {
1550
1826
  function emitMethodFacts(emitter, method) {
1551
1827
  const funcKey = nodeId(emitter.filePath, method);
1552
1828
  add(emitter, "func", funcKey);
1553
- const params = field(method, "parameters");
1554
- const declared = /* @__PURE__ */ new Set();
1555
1829
  let position = 0;
1556
- for (const param of params === null ? [] : children2(params)) {
1830
+ for (const param of parametersOf(method)) {
1557
1831
  const paramName = paramNameOf(param);
1558
1832
  if (paramName !== null) {
1559
- declared.add(paramName.text);
1560
1833
  const paramKey = `${funcKey}#${paramName.text}`;
1561
1834
  add(emitter, "paramOf", funcKey, String(position), paramKey);
1562
1835
  add(emitter, "paramNamed", funcKey, paramName.text, paramKey);
1563
1836
  }
1564
1837
  position += 1;
1565
1838
  }
1566
- const inside = {
1567
- ...emitter,
1568
- enclosing: { funcKey, params: declared }
1569
- };
1839
+ const inside = { ...emitter, enclosing: method };
1570
1840
  const body = field(method, "body");
1571
1841
  if (body === null) {
1572
1842
  return funcKey;
@@ -1598,31 +1868,78 @@ function emitMethodFacts(emitter, method) {
1598
1868
  add(inside, "returnsValue", funcKey, valueKey(inside, implicit));
1599
1869
  }
1600
1870
  emitExpressionFacts(inside, body);
1871
+ emitScopeWrites(inside, method, body);
1601
1872
  return funcKey;
1602
1873
  }
1603
- function emitAssignment(emitter, assignment2) {
1604
- const left = field(assignment2, "left");
1605
- const right = field(assignment2, "right");
1606
- if (right === null) {
1607
- return;
1874
+ function isConstruction(value) {
1875
+ return value.type === "call" || ARRAY_TYPES.has(value.type) || value.type === "hash";
1876
+ }
1877
+ function sourceOf(node) {
1878
+ return node.text.replace(/\s+/g, " ").trim();
1879
+ }
1880
+ function readFirst(node) {
1881
+ const inner = readThrough(node);
1882
+ if (inner !== node) {
1883
+ return inner;
1608
1884
  }
1609
- if (left?.type !== "identifier" && left?.type !== "constant") {
1610
- return;
1885
+ return node.type === "call" ? field(node, "receiver") : null;
1886
+ }
1887
+ var CHAIN_READS = {
1888
+ nameTypes: RUBY_NAME_TYPES,
1889
+ readFirst
1890
+ };
1891
+ function describeWrite(emitter, write) {
1892
+ const value = write.value === null ? null : readThrough(write.value);
1893
+ return {
1894
+ value: value === null ? null : valueKey(emitter, value),
1895
+ placeholder: value?.type === "nil",
1896
+ construction: value !== null && isConstruction(value) ? sourceOf(value) : null,
1897
+ narrowsName: value !== null && value.type === "call" && startsAtName(value, write.name, CHAIN_READS)
1898
+ };
1899
+ }
1900
+ function settledValue(emitter, group) {
1901
+ const only = group.writes.length === 1 ? group.writes[0] : void 0;
1902
+ if (only !== void 0) {
1903
+ return only.fromParameter || only.value === null ? null : valueKey(emitter, only.value);
1611
1904
  }
1612
- add(
1613
- emitter,
1614
- "binds",
1615
- nameId(emitter.filePath, left.text),
1616
- valueKey(emitter, right)
1617
- );
1618
- add(
1619
- emitter,
1620
- "exportsAs",
1621
- emitter.filePath,
1622
- left.text,
1623
- valueKey(emitter, right)
1905
+ return valueLeftByWrites(
1906
+ group.writes.map((write) => describeWrite(emitter, write)),
1907
+ group.ordered
1624
1908
  );
1625
1909
  }
1910
+ function emitScopeWrites(emitter, method, body) {
1911
+ for (const group of collectWrites(method, body)) {
1912
+ const settled = settledValue(emitter, group);
1913
+ const key = group.owner === null ? nameId(emitter.filePath, group.name) : `${nodeId(emitter.filePath, group.owner)}#${group.name}`;
1914
+ if (settled === null) {
1915
+ emitCandidates(emitter, key, group);
1916
+ continue;
1917
+ }
1918
+ add(
1919
+ emitter,
1920
+ group.writes.length === 1 ? "binds" : "endsHolding",
1921
+ key,
1922
+ settled
1923
+ );
1924
+ if (group.owner === null) {
1925
+ add(emitter, "exportsAs", emitter.filePath, group.name, settled);
1926
+ }
1927
+ }
1928
+ }
1929
+ function emitCandidates(emitter, key, group) {
1930
+ for (const write of group.writes) {
1931
+ if (write.value === null) {
1932
+ add(emitter, "writesUnstated", key);
1933
+ continue;
1934
+ }
1935
+ if (write.fromParameter) {
1936
+ continue;
1937
+ }
1938
+ if (!describeWrite(emitter, write).narrowsName) {
1939
+ add(emitter, "mayHold", key, valueKey(emitter, write.value));
1940
+ }
1941
+ }
1942
+ }
1626
1943
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1627
1944
  function emitClassFacts(emitter, cls) {
1628
1945
  const classKey = nodeId(emitter.filePath, cls);
@@ -1634,6 +1951,12 @@ function emitClassFacts(emitter, cls) {
1634
1951
  add(emitter, "extendsNamed", classKey, base.text);
1635
1952
  }
1636
1953
  const body = field(cls, "body");
1954
+ const collected = /* @__PURE__ */ new Map();
1955
+ const within = {
1956
+ ...emitter,
1957
+ selfKey: classKey,
1958
+ instanceWrites: collected
1959
+ };
1637
1960
  for (const statement of body === null ? [] : children2(body)) {
1638
1961
  if (statement.type === "assignment") {
1639
1962
  const left = field(statement, "left");
@@ -1657,16 +1980,23 @@ function emitClassFacts(emitter, cls) {
1657
1980
  if (!METHOD_TYPES.has(statement.type)) {
1658
1981
  continue;
1659
1982
  }
1660
- const funcKey = emitMethodFacts(emitter, statement);
1983
+ const funcKey = emitMethodFacts(within, statement);
1661
1984
  const name = field(statement, "name");
1662
1985
  if (name !== null) {
1663
1986
  add(emitter, "holdsProperty", classKey, name.text, funcKey);
1664
1987
  }
1665
1988
  }
1989
+ emitInstanceWrites(within, classKey, collected);
1666
1990
  return classKey;
1667
1991
  }
1668
1992
  function emitValueFacts(db, filePath, root) {
1669
- const emitter = { db, filePath, enclosing: null };
1993
+ const emitter = {
1994
+ db,
1995
+ filePath,
1996
+ enclosing: null,
1997
+ selfKey: null,
1998
+ instanceWrites: null
1999
+ };
1670
2000
  const declaresName = (child, key) => {
1671
2001
  const name = field(child, "name");
1672
2002
  if (name === null) {
@@ -1684,14 +2014,12 @@ function emitValueFacts(db, filePath, root) {
1684
2014
  if (METHOD_TYPES.has(child.type)) {
1685
2015
  declaresName(child, emitMethodFacts(emitter, child));
1686
2016
  }
1687
- if (child.type === "assignment") {
1688
- emitAssignment(emitter, child);
1689
- }
1690
2017
  walk(child);
1691
2018
  }
1692
2019
  };
1693
2020
  walk(root);
1694
2021
  emitExpressionFacts(emitter, root);
2022
+ emitScopeWrites(emitter, null, root);
1695
2023
  }
1696
2024
 
1697
2025
  // src/values/lowering.ts
@@ -1707,7 +2035,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1707
2035
  "module",
1708
2036
  "singleton_class"
1709
2037
  ]);
1710
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
2038
+ var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1711
2039
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1712
2040
  var NAME_TYPES = /* @__PURE__ */ new Set([
1713
2041
  "identifier",
@@ -2213,7 +2541,7 @@ function isTailExpression(node) {
2213
2541
  if (owner.type === "block" && owner.parent?.type === "lambda") {
2214
2542
  return true;
2215
2543
  }
2216
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
2544
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES3.has(owner.type)) {
2217
2545
  return true;
2218
2546
  }
2219
2547
  if (owner.type === "when") {
@@ -2232,7 +2560,7 @@ function isRoot(node) {
2232
2560
  if (OWN_ROOT_TYPES.has(node.type)) {
2233
2561
  return true;
2234
2562
  }
2235
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
2563
+ return BLOCK_TYPES3.has(node.type) && node.parent?.type === "call";
2236
2564
  }
2237
2565
  function isPathStatement(node, parent) {
2238
2566
  if (node.type === "elsif") {
@@ -2260,7 +2588,7 @@ function siteOf(node) {
2260
2588
  current = parent;
2261
2589
  }
2262
2590
  }
2263
- function parametersOf(parameters) {
2591
+ function parametersOf2(parameters) {
2264
2592
  if (parameters === null) {
2265
2593
  return [];
2266
2594
  }
@@ -2282,19 +2610,19 @@ function functionOf(node) {
2282
2610
  return null;
2283
2611
  }
2284
2612
  return {
2285
- parameters: parametersOf(field(node, "parameters")),
2613
+ parameters: parametersOf2(field(node, "parameters")),
2286
2614
  body: bodyOf(block)
2287
2615
  };
2288
2616
  }
2289
- if (BLOCK_TYPES2.has(node.type)) {
2617
+ if (BLOCK_TYPES3.has(node.type)) {
2290
2618
  return {
2291
- parameters: parametersOf(field(node, "parameters")),
2619
+ parameters: parametersOf2(field(node, "parameters")),
2292
2620
  body: bodyOf(node)
2293
2621
  };
2294
2622
  }
2295
2623
  if (METHOD_TYPES2.has(node.type)) {
2296
2624
  const body = field(node, "body");
2297
- const parameters = parametersOf(field(node, "parameters"));
2625
+ const parameters = parametersOf2(field(node, "parameters"));
2298
2626
  if (body === null) {
2299
2627
  return { parameters, body: [] };
2300
2628
  }
@@ -3507,13 +3835,8 @@ function namesUnder(node) {
3507
3835
  }
3508
3836
 
3509
3837
  // src/storage.ts
3510
- import { deriveOnDemand as deriveOnDemand2, evaluate as evaluate2 } from "@suss/datalog";
3511
3838
  import { storageBinding } from "@suss/ir-core";
3512
- import {
3513
- ANSWER_RELATIONS as ANSWER_RELATIONS2,
3514
- RESOLUTION_QUESTIONS as RESOLUTION_QUESTIONS2,
3515
- RESOLUTION_RULES as RESOLUTION_RULES2
3516
- } from "@suss/resolution";
3839
+ import { askResolution as askResolution2 } from "@suss/resolution";
3517
3840
  function children3(node) {
3518
3841
  return node.namedChildren.filter((child) => child !== null);
3519
3842
  }
@@ -3539,13 +3862,8 @@ function isConstant(node) {
3539
3862
  function constantName(constant2) {
3540
3863
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
3541
3864
  }
3542
- var ANCESTRY_PROGRAM = deriveOnDemand2(
3543
- [...RESOLUTION_RULES2, ...RESOLUTION_QUESTIONS2],
3544
- ANSWER_RELATIONS2
3545
- );
3546
3865
  function reachesBase(facts, classKey, bases) {
3547
- facts.add("wantedAncestry", [classKey]);
3548
- evaluate2(facts, ANCESTRY_PROGRAM.rules);
3866
+ askResolution2(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
3549
3867
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
3550
3868
  }
3551
3869
  function classBehind(facts, file, constant2) {
@@ -4483,7 +4801,8 @@ function collectFileConstants(filePath, root) {
4483
4801
  if (written !== null) {
4484
4802
  definitions.push({
4485
4803
  qualifiedName: qualify(nesting, written),
4486
- key: nodeId(filePath, node)
4804
+ key: nodeId(filePath, node),
4805
+ kind: "declaration"
4487
4806
  });
4488
4807
  const inside = [...nesting, ...written.split("::")];
4489
4808
  for (const child of children4(node)) {
@@ -4498,7 +4817,8 @@ function collectFileConstants(filePath, root) {
4498
4817
  if (left !== null && left.type === "constant" && right !== null) {
4499
4818
  definitions.push({
4500
4819
  qualifiedName: qualify(nesting, left.text),
4501
- key: nodeId(filePath, right)
4820
+ key: nodeId(filePath, right),
4821
+ kind: "assignment"
4502
4822
  });
4503
4823
  }
4504
4824
  }
@@ -4525,14 +4845,18 @@ function emitConstantBindings(db, perFile) {
4525
4845
  for (const file of files) {
4526
4846
  for (const definition of file.definitions) {
4527
4847
  const found = byName.get(definition.qualifiedName) ?? [];
4528
- found.push(definition.key);
4848
+ found.push(definition);
4529
4849
  byName.set(definition.qualifiedName, found);
4530
4850
  fileOfDefinition.set(definition.key, file.filePath);
4531
4851
  }
4532
4852
  }
4533
4853
  const settles = (candidate) => {
4534
- const found = byName.get(candidate);
4535
- return found !== void 0 && found.length === 1 ? found[0] ?? null : null;
4854
+ const found = byName.get(candidate) ?? [];
4855
+ const declared = found.filter((one) => one.kind === "declaration");
4856
+ if (declared.length > 0) {
4857
+ return declared[0].key;
4858
+ }
4859
+ return found.length === 1 ? found[0].key : null;
4536
4860
  };
4537
4861
  for (const file of files) {
4538
4862
  for (const reference of file.references) {
@@ -4559,7 +4883,7 @@ function emitConstantBindings(db, perFile) {
4559
4883
  import fs4 from "fs";
4560
4884
  import path5 from "path";
4561
4885
  import { Database as Database3 } from "@suss/datalog";
4562
- import { explainResolvedKey, RESOLUTION_RULES as RESOLUTION_RULES3 } from "@suss/resolution";
4886
+ import { explainResolvedKey, RESOLUTION_RULES } from "@suss/resolution";
4563
4887
 
4564
4888
  // src/parser.ts
4565
4889
  import fs2 from "fs";
@@ -4707,34 +5031,118 @@ import {
4707
5031
  unfollowedCallGap,
4708
5032
  worthRecording
4709
5033
  } from "@suss/behavioral-ir";
4710
- import { Database, evaluate as evaluate3, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
5034
+ import { Database, evaluate, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
4711
5035
  import { assembleSummary } from "@suss/extractor";
4712
5036
 
4713
5037
  // src/reach/resolveCallee.ts
5038
+ import { calleeOutcomeOf, calleeOutcomes } from "@suss/resolution";
4714
5039
  var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
5040
+ var INVOKES_RECEIVER = "call";
5041
+ var STOP_FOR = {
5042
+ severalSources: "multipleSources",
5043
+ outsideRun: "outsideRun",
5044
+ unsettled: "unsettledValue",
5045
+ undeclared: "noDeclaration"
5046
+ };
4715
5047
  var stop = (reason) => ({
4716
5048
  kind: "stopped",
4717
5049
  reason
4718
5050
  });
5051
+ var NO_DECLARATION = stop("noDeclaration");
4719
5052
  function followed(target) {
4720
5053
  return { kind: "followed", target };
4721
5054
  }
4722
- async function resolveCallee(call, site, ctx) {
5055
+ function calleeSpellings(calls, ctx) {
5056
+ const spellingOf = /* @__PURE__ */ new Map();
5057
+ const keys = /* @__PURE__ */ new Set();
5058
+ for (const { call, site } of calls) {
5059
+ const spelling = spellingFor(call, site);
5060
+ spellingOf.set(call.id, spelling);
5061
+ if (spelling.kind === "receiver") {
5062
+ keys.add(spelling.key);
5063
+ }
5064
+ }
5065
+ return {
5066
+ spellingOf,
5067
+ outcomes: calleeOutcomes(ctx.facts, [...keys], RUBY_PROGRAM)
5068
+ };
5069
+ }
5070
+ async function resolveCallee(call, site, ctx, read) {
5071
+ const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
5072
+ if (spelling.kind === "stopped") {
5073
+ return spelling;
5074
+ }
5075
+ if (spelling.kind === "implicitSelf") {
5076
+ return resolveImplicitSelf(spelling.name, site, ctx);
5077
+ }
5078
+ const outcome = read?.outcomes.get(spelling.key) ?? calleeOutcomeOf(ctx.facts, spelling.key, RUBY_PROGRAM);
5079
+ return asCallee(spelling, outcome, site, ctx);
5080
+ }
5081
+ function spellingFor(call, site) {
4723
5082
  const methodName = calleeMethodName(call);
4724
5083
  if (methodName === void 0) {
4725
- return stop("noDeclaration");
5084
+ return { kind: "stopped", reason: "noDeclaration" };
4726
5085
  }
4727
5086
  if (DYNAMIC_SEND_NAMES.has(methodName)) {
4728
- return stop("unsettledValue");
5087
+ return { kind: "stopped", reason: "unsettledValue" };
4729
5088
  }
4730
5089
  const receiver = field(call, "receiver");
4731
5090
  if (receiver === null || receiver.type === "self") {
4732
- return resolveImplicitSelf(methodName, site, ctx);
5091
+ return { kind: "implicitSelf", name: methodName };
5092
+ }
5093
+ return {
5094
+ kind: "receiver",
5095
+ key: readKey(site.file, receiver, site.method),
5096
+ method: methodName,
5097
+ onClassItself: receiver.type === "constant" || receiver.type === "scope_resolution"
5098
+ };
5099
+ }
5100
+ async function asCallee(spelling, outcome, site, ctx) {
5101
+ if (outcome.kind === "severalSources") {
5102
+ return stop("multipleSources");
5103
+ }
5104
+ if (outcome.kind === "callerSupplied") {
5105
+ return fromCaller(spelling, outcome.key, site);
5106
+ }
5107
+ const objects = objectsBehind(ctx.facts, spelling.key);
5108
+ if (objects.length > 1) {
5109
+ return stop("multipleSources");
5110
+ }
5111
+ const objectKey = objects[0] ?? (outcome.kind === "object" ? outcome.key : void 0);
5112
+ if (objectKey !== void 0) {
5113
+ return methodOnObject(spelling, objectKey, ctx);
4733
5114
  }
4734
- if (receiver.type === "identifier" && methodName === "call" && site.ownParameters.includes(receiver.text)) {
4735
- return stop("callerSupplied");
5115
+ if (outcome.kind === "function") {
5116
+ return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
4736
5117
  }
4737
- return resolveOnReceiver(receiver, methodName, site, ctx);
5118
+ return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
5119
+ }
5120
+ function objectsBehind(facts, key) {
5121
+ return [
5122
+ ...new Set(
5123
+ facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
5124
+ )
5125
+ ];
5126
+ }
5127
+ async function methodOnObject(spelling, objectKey, ctx) {
5128
+ const qualifiedName = ctx.classNames.get(objectKey);
5129
+ if (qualifiedName === void 0) {
5130
+ return NO_DECLARATION;
5131
+ }
5132
+ if (!spelling.onClassItself) {
5133
+ return methodOnAncestryOf(qualifiedName, spelling.method, ctx);
5134
+ }
5135
+ return spelling.method === "new" ? methodOnAncestryOf(qualifiedName, "initialize", ctx) : singletonMethodOn(qualifiedName, spelling.method, ctx);
5136
+ }
5137
+ function fromCaller(spelling, parameterKey, site) {
5138
+ if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKES_RECEIVER) {
5139
+ return stop("unsettledValue");
5140
+ }
5141
+ return stop("callerSupplied");
5142
+ }
5143
+ function functionCallee(key, ctx) {
5144
+ const target = ctx.definitions.get(key);
5145
+ return target === void 0 ? NO_DECLARATION : followed(target);
4738
5146
  }
4739
5147
  async function resolveMethodReference(name, site, ctx) {
4740
5148
  const resolved = await resolveImplicitSelf(name, site, ctx);
@@ -4759,58 +5167,13 @@ async function resolveImplicitSelf(methodName, site, ctx) {
4759
5167
  function resolveTopLevelName(methodName, ctx) {
4760
5168
  const candidates = ctx.topLevelMethods.get(methodName);
4761
5169
  if (candidates === void 0 || candidates.length === 0) {
4762
- return stop("noDeclaration");
5170
+ return NO_DECLARATION;
4763
5171
  }
4764
5172
  if (candidates.length > 1) {
4765
5173
  return stop("multipleSources");
4766
5174
  }
4767
5175
  return followed(candidates[0]);
4768
5176
  }
4769
- async function resolveOnReceiver(receiver, methodName, site, ctx) {
4770
- if (receiver.type === "call") {
4771
- const innerName = calleeMethodName(receiver);
4772
- const innerReceiver = field(receiver, "receiver");
4773
- if (innerName === "new" && innerReceiver !== null) {
4774
- const classRef = classRefOf(innerReceiver, site, ctx);
4775
- return classRef === null ? stop("unsettledValue") : methodOnAncestryOf(classRef, methodName, ctx);
4776
- }
4777
- const root = rootConstantOf(receiver);
4778
- if (root === null) {
4779
- return stop("unsettledValue");
4780
- }
4781
- return classRefOf(root, site, ctx) === null ? stop("outsideRun") : stop("unsettledValue");
4782
- }
4783
- if (receiver.type === "constant" || receiver.type === "scope_resolution") {
4784
- const classRef = classRefOf(receiver, site, ctx);
4785
- return classRef === null ? stop("outsideRun") : singletonMethodOn(classRef, methodName, ctx);
4786
- }
4787
- return stop("unsettledValue");
4788
- }
4789
- function rootConstantOf(node) {
4790
- if (node.type === "constant" || node.type === "scope_resolution") {
4791
- return node;
4792
- }
4793
- if (node.type !== "call") {
4794
- return null;
4795
- }
4796
- const receiver = field(node, "receiver");
4797
- return receiver === null ? null : rootConstantOf(receiver);
4798
- }
4799
- function classRefOf(node, site, ctx) {
4800
- if (node.type === "constant") {
4801
- const shadow = shadowingClassFor(node, site.nesting, ctx.knownClasses);
4802
- if (shadow !== null) {
4803
- return shadow;
4804
- }
4805
- const qualified = qualifyConstantRef(node, site.nesting);
4806
- return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
4807
- }
4808
- if (node.type === "scope_resolution") {
4809
- const qualified = qualifyConstantRef(node, site.nesting);
4810
- return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
4811
- }
4812
- return null;
4813
- }
4814
5177
  async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
4815
5178
  const ownBlocks = ctx.lookup.localDefinition?.(qualifiedName) ?? null;
4816
5179
  if (ownBlocks === null) {
@@ -4826,7 +5189,7 @@ async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
4826
5189
  found.cause === "dynamicDefine" ? "unsettledValue" : "outsideRun"
4827
5190
  );
4828
5191
  }
4829
- return stop("noDeclaration");
5192
+ return NO_DECLARATION;
4830
5193
  }
4831
5194
  function reachedMethod(node, block, name) {
4832
5195
  return {
@@ -4851,7 +5214,7 @@ function singletonMethodOn(qualifiedName, methodName, ctx) {
4851
5214
  return followed(reachedMethod(found, block, methodName));
4852
5215
  }
4853
5216
  }
4854
- return stop("noDeclaration");
5217
+ return NO_DECLARATION;
4855
5218
  }
4856
5219
 
4857
5220
  // src/reach/closure.ts
@@ -4864,7 +5227,7 @@ var REACHABLE_RULES = [
4864
5227
  )
4865
5228
  ];
4866
5229
  async function reachedFunctions(seeds, options) {
4867
- const ctx = buildReachContext(options.files);
5230
+ const ctx = buildReachContext(options.files, options.facts);
4868
5231
  const db = new Database();
4869
5232
  const functionByKey = /* @__PURE__ */ new Map();
4870
5233
  const seedKeys = /* @__PURE__ */ new Set();
@@ -4886,18 +5249,26 @@ async function reachedFunctions(seeds, options) {
4886
5249
  db.add("entry", [seed.key]);
4887
5250
  }
4888
5251
  for (; ; ) {
4889
- evaluate3(db, REACHABLE_RULES);
5252
+ evaluate(db, REACHABLE_RULES);
4890
5253
  const frontier = db.facts("reachable").map(([key]) => String(key)).filter((key) => !scanned.has(key));
4891
5254
  if (frontier.length === 0) {
4892
5255
  break;
4893
5256
  }
4894
- for (const key of frontier) {
5257
+ const bodies = frontier.flatMap((key) => {
4895
5258
  scanned.add(key);
4896
5259
  const source = functionByKey.get(key);
4897
- if (source === void 0) {
4898
- continue;
4899
- }
4900
- const scan = await scanBody(source, ctx, options);
5260
+ return source === void 0 ? [] : [{ key, source, ...bodyOf2(source, options) }];
5261
+ });
5262
+ const spellings = calleeSpellings(
5263
+ bodies.flatMap((body) => body.written),
5264
+ ctx
5265
+ );
5266
+ for (const { key, source, calls, site } of bodies) {
5267
+ const scan = await scanBody(source, ctx, options, {
5268
+ calls,
5269
+ site,
5270
+ spellings
5271
+ });
4901
5272
  if (scan.stops.length > 0) {
4902
5273
  stopsByKey.set(key, scan.stops);
4903
5274
  }
@@ -4950,11 +5321,22 @@ async function reachedFunctions(seeds, options) {
4950
5321
  function keyOf(target) {
4951
5322
  return nodeId(target.file, target.node);
4952
5323
  }
4953
- function buildReachContext(files) {
5324
+ var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5325
+ function buildReachContext(files, facts) {
4954
5326
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
4955
5327
  const classes = [];
5328
+ const classNames = /* @__PURE__ */ new Map();
5329
+ const classOfMethod = /* @__PURE__ */ new Map();
4956
5330
  for (const { file, root } of files) {
4957
- walkDefinitions(root, (info) => classes.push({ file, info }));
5331
+ walkDefinitions(root, (info) => {
5332
+ classes.push({ file, info });
5333
+ classNames.set(nodeId(file, info.node), info.qualifiedName);
5334
+ for (const statement of namedChildren(info.bodyNode)) {
5335
+ if (METHOD_TYPES4.has(statement.type)) {
5336
+ classOfMethod.set(nodeId(file, statement), info.qualifiedName);
5337
+ }
5338
+ }
5339
+ });
4958
5340
  }
4959
5341
  const knownClasses = new Set(classes.map(({ info }) => info.qualifiedName));
4960
5342
  for (const { file, info } of classes) {
@@ -4962,22 +5344,33 @@ function buildReachContext(files) {
4962
5344
  list.push({ info, knownClasses, file });
4963
5345
  blocksByQualifiedName.set(info.qualifiedName, list);
4964
5346
  }
4965
- const topLevelMethods = /* @__PURE__ */ new Map();
5347
+ const definitions = /* @__PURE__ */ new Map();
4966
5348
  for (const { file, root } of files) {
4967
- for (const method of topLevelMethodNodes(root)) {
5349
+ for (const [key, method] of methodDefinitionsIn(file, root)) {
4968
5350
  const name = field(method, "name")?.text;
4969
5351
  if (name === void 0) {
4970
5352
  continue;
4971
5353
  }
4972
- const list = topLevelMethods.get(name) ?? [];
4973
- list.push({
5354
+ const owner = classOfMethod.get(key) ?? null;
5355
+ definitions.set(key, {
4974
5356
  file,
4975
5357
  node: method,
4976
5358
  name,
4977
- exportPath: [name],
4978
- enclosingQualifiedName: null
5359
+ exportPath: owner === null ? [name] : [owner, name],
5360
+ enclosingQualifiedName: owner
4979
5361
  });
4980
- topLevelMethods.set(name, list);
5362
+ }
5363
+ }
5364
+ const topLevelMethods = /* @__PURE__ */ new Map();
5365
+ for (const { file, root } of files) {
5366
+ for (const method of topLevelMethodNodes(root)) {
5367
+ const found = definitions.get(nodeId(file, method));
5368
+ if (found === void 0) {
5369
+ continue;
5370
+ }
5371
+ const list = topLevelMethods.get(found.name) ?? [];
5372
+ list.push(found);
5373
+ topLevelMethods.set(found.name, list);
4981
5374
  }
4982
5375
  }
4983
5376
  const lookup = {
@@ -4990,7 +5383,13 @@ function buildReachContext(files) {
4990
5383
  parsedFile: async () => null,
4991
5384
  localDefinition: (name) => blocksByQualifiedName.get(name) ?? null
4992
5385
  };
4993
- return { lookup, knownClasses, topLevelMethods };
5386
+ return { lookup, topLevelMethods, facts, classNames, definitions };
5387
+ }
5388
+ function namedChildren(node) {
5389
+ if (node === null) {
5390
+ return [];
5391
+ }
5392
+ return node.namedChildren.filter((child) => child !== null);
4994
5393
  }
4995
5394
  function topLevelMethodNodes(root, found = []) {
4996
5395
  for (const child of bodyStatements(root)) {
@@ -5005,14 +5404,6 @@ function topLevelMethodNodes(root, found = []) {
5005
5404
  }
5006
5405
  return found;
5007
5406
  }
5008
- function nestingOf(qualifiedName) {
5009
- const parts = qualifiedName.split("::");
5010
- const out = [];
5011
- for (let depth = parts.length; depth >= 1; depth--) {
5012
- out.push(parts.slice(0, depth).join("::"));
5013
- }
5014
- return out;
5015
- }
5016
5407
  var EMPTY_SCAN = {
5017
5408
  followed: [],
5018
5409
  stops: [],
@@ -5030,21 +5421,23 @@ function methodReferenceSymbol(node) {
5030
5421
  const argChildren = args === null ? [] : bodyStatements(args);
5031
5422
  return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
5032
5423
  }
5033
- async function scanBody(source, ctx, options) {
5424
+ function bodyOf2(source, options) {
5425
+ const site = {
5426
+ file: source.file,
5427
+ method: source.node,
5428
+ owner: keyOf(source),
5429
+ enclosingQualifiedName: source.enclosingQualifiedName
5430
+ };
5431
+ const calls = field(source.node, "body") === null ? [] : withoutChainLinks(bodyCalls(source.node, options.inheritedMethods));
5432
+ return { calls, site, written: calls.map((call) => ({ call, site })) };
5433
+ }
5434
+ async function scanBody(source, ctx, options, read) {
5034
5435
  const displayPathOf = options.displayPathOf;
5035
- const body = field(source.node, "body");
5036
- if (body === null) {
5436
+ if (field(source.node, "body") === null) {
5037
5437
  return EMPTY_SCAN;
5038
5438
  }
5039
- const calls = withoutChainLinks(
5040
- bodyCalls(source.node, options.inheritedMethods)
5041
- );
5439
+ const { calls, site } = read;
5042
5440
  const ownParameters = positionalParameters(source.node).map((p) => p.name);
5043
- const site = {
5044
- enclosingQualifiedName: source.enclosingQualifiedName,
5045
- nesting: source.enclosingQualifiedName === null ? [] : nestingOf(source.enclosingQualifiedName),
5046
- ownParameters
5047
- };
5048
5441
  const followed2 = [];
5049
5442
  const stops = [];
5050
5443
  const placements = new TargetPlacements();
@@ -5087,7 +5480,7 @@ async function scanBody(source, ctx, options) {
5087
5480
  };
5088
5481
  for (const call of calls) {
5089
5482
  const callee = calleeText(call);
5090
- const outcome = await resolveCallee(call, site, ctx);
5483
+ const outcome = await resolveCallee(call, site, ctx, read.spellings);
5091
5484
  const placed = outcome.kind === "followed" ? {
5092
5485
  file: displayPathOf(outcome.target.file),
5093
5486
  span: spanOf(outcome.target.node)
@@ -5386,6 +5779,7 @@ async function extractRubyProject(options) {
5386
5779
  () => reachedFunctions(seeds, {
5387
5780
  files: parsed,
5388
5781
  displayPathOf,
5782
+ facts: db,
5389
5783
  ...storage === void 0 ? {} : { storage },
5390
5784
  inheritedMethods
5391
5785
  })
@@ -5482,15 +5876,15 @@ function findRubyFiles(root) {
5482
5876
  }
5483
5877
 
5484
5878
  // src/facts/why.ts
5485
- var WITNESS_RULES = [...RESOLUTION_RULES3, ...RUBY_RULES];
5486
- var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5879
+ var WITNESS_RULES = [...RESOLUTION_RULES, ...RUBY_RULES];
5880
+ var METHOD_TYPES5 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5487
5881
  function namedChildrenOf(node) {
5488
5882
  return node.namedChildren.filter((child) => child !== null);
5489
5883
  }
5490
5884
  function enclosingMethod2(node) {
5491
5885
  let current = node.parent;
5492
5886
  while (current !== null) {
5493
- if (METHOD_TYPES4.has(current.type)) {
5887
+ if (METHOD_TYPES5.has(current.type)) {
5494
5888
  return current;
5495
5889
  }
5496
5890
  current = current.parent;