@suss/adapter-ruby 0.26.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.cjs CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  inheritedStatements: () => inheritedStatements,
52
52
  methodDefinitionsIn: () => methodDefinitionsIn,
53
53
  methodInAncestry: () => methodInAncestry,
54
+ nestedStatements: () => nestedStatements,
54
55
  nodeId: () => nodeId,
55
56
  parseRuby: () => parseRuby,
56
57
  parseRubySync: () => parseRubySync,
@@ -94,6 +95,45 @@ function field(node, name) {
94
95
  function bodyStatements(body) {
95
96
  return body.namedChildren.filter((child) => child !== null);
96
97
  }
98
+ var BRANCH_FIELDS = {
99
+ if: ["consequence", "alternative"],
100
+ unless: ["consequence", "alternative"],
101
+ elsif: ["consequence", "alternative"],
102
+ if_modifier: ["body"],
103
+ unless_modifier: ["body"],
104
+ while: ["body"],
105
+ until: ["body"],
106
+ while_modifier: ["body"],
107
+ until_modifier: ["body"],
108
+ when: ["body"],
109
+ rescue: ["body"]
110
+ };
111
+ var STATEMENT_LISTS = /* @__PURE__ */ new Set(["then", "else", "do", "ensure", "begin"]);
112
+ var CLAUSE_TYPES = /* @__PURE__ */ new Set(["elsif", "else", "when", "rescue", "ensure"]);
113
+ function nestedStatements(stmt) {
114
+ const branchFields = BRANCH_FIELDS[stmt.type];
115
+ if (branchFields !== void 0) {
116
+ return branchFields.flatMap((name) => {
117
+ const branch = field(stmt, name);
118
+ return branch === null ? [] : statementsRunBy(branch);
119
+ });
120
+ }
121
+ if (STATEMENT_LISTS.has(stmt.type)) {
122
+ return bodyStatements(stmt).flatMap(
123
+ (child) => CLAUSE_TYPES.has(child.type) ? nestedStatements(child) : [child]
124
+ );
125
+ }
126
+ if (stmt.type === "case") {
127
+ return bodyStatements(stmt).filter((child) => CLAUSE_TYPES.has(child.type)).flatMap(nestedStatements);
128
+ }
129
+ return [];
130
+ }
131
+ function statementsRunBy(branch) {
132
+ if (STATEMENT_LISTS.has(branch.type) || CLAUSE_TYPES.has(branch.type)) {
133
+ return nestedStatements(branch);
134
+ }
135
+ return [branch];
136
+ }
97
137
  var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
98
138
  "method",
99
139
  "singleton_method",
@@ -157,8 +197,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
157
197
  "protected",
158
198
  "public"
159
199
  ]);
160
- function visibilityKeyword(text) {
161
- return VISIBILITY_KEYWORDS.has(text) ? text : null;
200
+ function visibilityKeyword(text2) {
201
+ return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
162
202
  }
163
203
  function setVisibility(visibility, name, keyword) {
164
204
  if (keyword === "public") {
@@ -281,14 +321,31 @@ var NodeMap = class extends import_extractor.IdMap {
281
321
  // src/constantPath.ts
282
322
  var import_node_fs = __toESM(require("fs"), 1);
283
323
  var import_node_path = __toESM(require("path"), 1);
284
- function underscoreConstantPath(qualifiedName) {
285
- return qualifiedName.replaceAll("::", "/").replace(/([A-Z\d]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
324
+ function underscoreConstantPath(qualifiedName, acronyms = []) {
325
+ return lowerAcronyms(qualifiedName.replaceAll("::", "/"), acronyms).replace(/([A-Z\d]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
326
+ }
327
+ function lowerAcronyms(word, acronyms) {
328
+ if (acronyms.length === 0) {
329
+ return word;
330
+ }
331
+ const alternatives = acronyms.map(escapeRegExp).join("|");
332
+ const acronymRegex = new RegExp(
333
+ `(?:(?<=([A-Za-z\\d]))|\\b)(${alternatives})(?=\\b|[^a-z])`,
334
+ "g"
335
+ );
336
+ return word.replace(
337
+ acronymRegex,
338
+ (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
339
+ );
340
+ }
341
+ function escapeRegExp(text2) {
342
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
343
  }
287
344
  var PATH_CONVENTIONS = {
288
345
  railsUnderscore: underscoreConstantPath
289
346
  };
290
- function resolveConstantFile(root, qualifiedName, convention) {
291
- const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
347
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = []) {
348
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
292
349
  for (const candidate of [root, ...autoloadDirectories(root)]) {
293
350
  const file = import_node_path.default.join(candidate, relative);
294
351
  if (import_node_fs.default.existsSync(file)) {
@@ -362,12 +419,19 @@ function walkBody(statements, nesting, visit) {
362
419
  }
363
420
  }
364
421
  function ownIdentity(nameNode, outerNesting) {
365
- const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
422
+ const qualifiedName = definedName(nameNode, outerNesting);
366
423
  if (qualifiedName === null) {
367
424
  return null;
368
425
  }
369
426
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
370
427
  }
428
+ function definedName(nameNode, outerNesting) {
429
+ const innermost = outerNesting[0] ?? null;
430
+ if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
431
+ return `${innermost}::${nameNode.text}`;
432
+ }
433
+ return qualifyConstantRef(nameNode, outerNesting);
434
+ }
371
435
  function visitClass(node, nesting, visit) {
372
436
  const nameNode = field(node, "name");
373
437
  const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
@@ -431,7 +495,8 @@ async function reachDefinition(qualifiedName, lookup) {
431
495
  const filePath = resolveConstantFile(
432
496
  lookup.root,
433
497
  qualifiedName,
434
- lookup.pathConvention
498
+ lookup.pathConvention,
499
+ lookup.acronyms
435
500
  );
436
501
  if (filePath === null) {
437
502
  return null;
@@ -473,7 +538,10 @@ async function chainOf(self, lookup, active) {
473
538
  async function superclassChain(self, lookup, active) {
474
539
  const candidates = superclassCandidatesOf(self.blocks);
475
540
  for (const candidate of candidates) {
476
- if (lookup.ancestryRootClassNames.includes(candidate) || active.has(candidate)) {
541
+ if (lookup.ancestryRootClassNames.includes(candidate)) {
542
+ return [{ type: "root", name: candidate }];
543
+ }
544
+ if (active.has(candidate)) {
477
545
  return [];
478
546
  }
479
547
  const blocks = await definitionOf(candidate, lookup);
@@ -551,6 +619,9 @@ function superclassCandidatesOf(blocks) {
551
619
  }
552
620
  function methodInAncestry(ancestry, name) {
553
621
  for (const entry of ancestry) {
622
+ if (entry.type === "root") {
623
+ return { type: "none" };
624
+ }
554
625
  if (entry.type === "unfollowed") {
555
626
  return {
556
627
  type: "unsettled",
@@ -1275,7 +1346,7 @@ function calledMethod(node) {
1275
1346
  }
1276
1347
 
1277
1348
  // src/values/evaluator.ts
1278
- var import_resolution2 = require("@suss/resolution");
1349
+ var import_resolution4 = require("@suss/resolution");
1279
1350
  var import_values2 = require("@suss/values");
1280
1351
 
1281
1352
  // src/facts/resolve.ts
@@ -1295,18 +1366,9 @@ var RUBY_RULES = [
1295
1366
  ]
1296
1367
  )
1297
1368
  ];
1298
- var RESOLUTION_PROGRAM = (0, import_datalog.deriveOnDemand)(
1299
- [...import_resolution.RESOLUTION_RULES, ...RUBY_RULES, ...import_resolution.RESOLUTION_QUESTIONS],
1300
- import_resolution.ANSWER_RELATIONS
1301
- );
1369
+ var RUBY_PROGRAM = (0, import_resolution.resolutionProgram)(RUBY_RULES);
1302
1370
  function resolveValues(db, keys) {
1303
- if (keys.length === 0) {
1304
- return;
1305
- }
1306
- for (const key of keys) {
1307
- db.add("wanted", [key]);
1308
- }
1309
- (0, import_datalog.evaluate)(db, RESOLUTION_PROGRAM.rules);
1371
+ (0, import_resolution.askResolution)(db, keys, "wanted", RUBY_PROGRAM);
1310
1372
  }
1311
1373
  function resolvedFunctions(db, key) {
1312
1374
  return db.facts("wantedResolves").filter((row) => String(row[0]) === key).map((row) => String(row[1]));
@@ -1317,27 +1379,262 @@ function writtenValueOf(db, key) {
1317
1379
  }
1318
1380
 
1319
1381
  // src/facts/values.ts
1320
- function nodeId(filePath, node) {
1321
- return `${filePath}:${node.startIndex}-${node.endIndex}`;
1322
- }
1323
- function nameId(filePath, name) {
1324
- return `${filePath}#${name}`;
1325
- }
1382
+ var import_resolution3 = require("@suss/resolution");
1383
+
1384
+ // src/facts/locals.ts
1385
+ var import_resolution2 = require("@suss/resolution");
1386
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1387
+ var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES2]);
1388
+ var RUBY_NAME_TYPES = /* @__PURE__ */ new Set([
1389
+ "identifier",
1390
+ "instance_variable"
1391
+ ]);
1392
+ var TARGET_LIST_TYPES = /* @__PURE__ */ new Set([
1393
+ "left_assignment_list",
1394
+ "destructured_left_assignment",
1395
+ "rest_assignment"
1396
+ ]);
1397
+ var WHOLE_VALUE_OPERATORS = /* @__PURE__ */ new Set([
1398
+ "||=",
1399
+ "&&="
1400
+ ]);
1326
1401
  function paramNameOf(param) {
1327
1402
  return param.type === "identifier" ? param : field(param, "name") ?? null;
1328
1403
  }
1329
- function declaredParamNames(method) {
1330
- const params = field(method, "parameters");
1331
- const declared = /* @__PURE__ */ new Set();
1332
- for (const param of params === null ? [] : children2(params)) {
1404
+ function parametersOf(owner) {
1405
+ const params = field(owner, "parameters");
1406
+ return params === null ? [] : bodyStatements(params);
1407
+ }
1408
+ function parameterNames2(owner) {
1409
+ const names = /* @__PURE__ */ new Set();
1410
+ for (const param of parametersOf(owner)) {
1411
+ const name = paramNameOf(param);
1412
+ if (name !== null) {
1413
+ names.add(name.text);
1414
+ }
1415
+ }
1416
+ return names;
1417
+ }
1418
+ function targetsOf(left) {
1419
+ if (left.type === "identifier" || left.type === "constant") {
1420
+ return [left];
1421
+ }
1422
+ if (TARGET_LIST_TYPES.has(left.type)) {
1423
+ return bodyStatements(left).flatMap(targetsOf);
1424
+ }
1425
+ return [];
1426
+ }
1427
+ function writeOf(target, value, at, fromParameter) {
1428
+ return { name: target.text, target, value, at, fromParameter };
1429
+ }
1430
+ function readAssignment(node) {
1431
+ const left = field(node, "left");
1432
+ const right = field(node, "right");
1433
+ if (left === null || right === null) {
1434
+ return [];
1435
+ }
1436
+ const value = left.type === "left_assignment_list" ? null : right;
1437
+ return targetsOf(left).map((target) => writeOf(target, value, node, false));
1438
+ }
1439
+ function readOperatorAssignment(node) {
1440
+ const left = field(node, "left");
1441
+ const right = field(node, "right");
1442
+ const operator = field(node, "operator");
1443
+ if (left === null || right === null || left.type !== "identifier") {
1444
+ return [];
1445
+ }
1446
+ const whole = operator !== null && WHOLE_VALUE_OPERATORS.has(operator.text);
1447
+ return [writeOf(left, whole ? right : null, node, false)];
1448
+ }
1449
+ function readForVariable(node) {
1450
+ const pattern = field(node, "pattern");
1451
+ if (pattern === null) {
1452
+ return [];
1453
+ }
1454
+ return targetsOf(pattern).map((target) => writeOf(target, null, node, false));
1455
+ }
1456
+ function readBlockParameters(node) {
1457
+ const found = [];
1458
+ for (const param of parametersOf(node)) {
1333
1459
  const name = paramNameOf(param);
1334
1460
  if (name !== null) {
1335
- declared.add(name.text);
1461
+ found.push(writeOf(name, null, param, true));
1462
+ }
1463
+ }
1464
+ return found;
1465
+ }
1466
+ var WRITE_READERS = {
1467
+ assignment: readAssignment,
1468
+ operator_assignment: readOperatorAssignment,
1469
+ for: readForVariable,
1470
+ block: readBlockParameters,
1471
+ do_block: readBlockParameters
1472
+ };
1473
+ function writesUnder(body) {
1474
+ const found = [];
1475
+ const visit = (node) => {
1476
+ for (const child of bodyStatements(node)) {
1477
+ if (OWN_BODY_TYPES.has(child.type)) {
1478
+ continue;
1479
+ }
1480
+ found.push(...WRITE_READERS[child.type]?.(child) ?? []);
1481
+ visit(child);
1336
1482
  }
1483
+ };
1484
+ visit(body);
1485
+ return found;
1486
+ }
1487
+ var namesByTree = /* @__PURE__ */ new WeakMap();
1488
+ function cacheFor(tree) {
1489
+ const known = namesByTree.get(tree);
1490
+ if (known !== void 0) {
1491
+ return known;
1492
+ }
1493
+ const fresh = { file: null, byMethod: new NodeMap() };
1494
+ namesByTree.set(tree, fresh);
1495
+ return fresh;
1496
+ }
1497
+ function scopeNames(parameters, body) {
1498
+ const locals = new Set(parameters);
1499
+ const blockParams = /* @__PURE__ */ new Set();
1500
+ for (const write of body === null ? [] : writesUnder(body)) {
1501
+ if (write.fromParameter) {
1502
+ blockParams.add(write.name);
1503
+ continue;
1504
+ }
1505
+ locals.add(write.name);
1506
+ }
1507
+ return { locals, blockParams };
1508
+ }
1509
+ function namesOfMethod(method) {
1510
+ const cache = cacheFor(method.tree);
1511
+ const remembered = cache.byMethod.get(method);
1512
+ if (remembered !== void 0) {
1513
+ return remembered;
1337
1514
  }
1338
- return declared;
1515
+ const names = scopeNames(parameterNames2(method), field(method, "body"));
1516
+ cache.byMethod.set(method, names);
1517
+ return names;
1518
+ }
1519
+ function namesOfFile(tree) {
1520
+ const cache = cacheFor(tree);
1521
+ cache.file ??= scopeNames(/* @__PURE__ */ new Set(), tree.rootNode);
1522
+ return cache.file;
1339
1523
  }
1340
- function readKey(filePath, node, enclosing) {
1524
+ function ownerOfName(node, name, enclosing) {
1525
+ if (node.type === "constant") {
1526
+ return null;
1527
+ }
1528
+ const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
1529
+ if (names.blockParams.has(name)) {
1530
+ const block = blockDeclaring(node, name);
1531
+ if (block !== null) {
1532
+ return block;
1533
+ }
1534
+ }
1535
+ return enclosing !== null && names.locals.has(name) ? enclosing : null;
1536
+ }
1537
+ function blockDeclaring(node, name) {
1538
+ let current = node.parent;
1539
+ while (current !== null) {
1540
+ if (BLOCK_TYPES2.has(current.type) && parameterNames2(current).has(name)) {
1541
+ return current;
1542
+ }
1543
+ if (OWN_BODY_TYPES.has(current.type)) {
1544
+ return null;
1545
+ }
1546
+ current = current.parent;
1547
+ }
1548
+ return null;
1549
+ }
1550
+ function parameterWrites(method) {
1551
+ const found = [];
1552
+ for (const param of method === null ? [] : parametersOf(method)) {
1553
+ const name = paramNameOf(param);
1554
+ if (name !== null) {
1555
+ found.push(writeOf(name, name, param, true));
1556
+ }
1557
+ }
1558
+ return found;
1559
+ }
1560
+ function collectWrites(method, body) {
1561
+ const writes = [...parameterWrites(method), ...writesUnder(body)].sort(
1562
+ (left, right) => left.target.startIndex - right.target.startIndex
1563
+ );
1564
+ const targetIds = new Set(writes.map((write) => write.target.id));
1565
+ const groups = /* @__PURE__ */ new Map();
1566
+ for (const write of writes) {
1567
+ const owner = ownerOfName(write.target, write.name, method);
1568
+ const key = `${owner === null ? "" : owner.id} ${write.name}`;
1569
+ const group = groups.get(key);
1570
+ if (group === void 0) {
1571
+ groups.set(key, {
1572
+ owner,
1573
+ name: write.name,
1574
+ writes: [write],
1575
+ ordered: false
1576
+ });
1577
+ continue;
1578
+ }
1579
+ group.writes.push(write);
1580
+ }
1581
+ const rules = nameReads(targetIds);
1582
+ for (const group of groups.values()) {
1583
+ const scope = group.owner === null ? body : field(group.owner, "body");
1584
+ if (scope === null) {
1585
+ continue;
1586
+ }
1587
+ group.ordered = (0, import_resolution2.writesRunInOrder)(
1588
+ scope,
1589
+ group.name,
1590
+ group.writes.map((write) => orderedWrite(write, scope)),
1591
+ rules
1592
+ );
1593
+ }
1594
+ return [...groups.values()];
1595
+ }
1596
+ function orderedWrite(write, scope) {
1597
+ return {
1598
+ at: write.at,
1599
+ direct: !write.fromParameter && write.at.parent?.id === scope.id
1600
+ };
1601
+ }
1602
+ function nameReads(targetIds) {
1603
+ return {
1604
+ nameTypes: RUBY_NAME_TYPES,
1605
+ laterBodies: LATER_BODY_TYPES,
1606
+ childrenOf: bodyStatements,
1607
+ isRead: (node) => isNameRead(node, targetIds)
1608
+ };
1609
+ }
1610
+ function isNameRead(node, targetIds) {
1611
+ if (targetIds.has(node.id)) {
1612
+ return false;
1613
+ }
1614
+ const parent = node.parent;
1615
+ return !(parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null);
1616
+ }
1617
+
1618
+ // src/facts/values.ts
1619
+ function nodeId(filePath, node) {
1620
+ return `${filePath}:${node.startIndex}-${node.endIndex}`;
1621
+ }
1622
+ function nameId(filePath, name) {
1623
+ return `${filePath}#${name}`;
1624
+ }
1625
+ function readThrough(node) {
1626
+ if (node.type !== "parenthesized_statements") {
1627
+ return node;
1628
+ }
1629
+ const inner = node.namedChildren.filter((child) => child !== null);
1630
+ return inner.length === 1 ? readThrough(inner[0]) : node;
1631
+ }
1632
+ function nameKey(filePath, node, enclosing) {
1633
+ const owner = ownerOfName(node, node.text, enclosing);
1634
+ return owner === null ? nameId(filePath, node.text) : `${nodeId(filePath, owner)}#${node.text}`;
1635
+ }
1636
+ function readKey(filePath, written, enclosing) {
1637
+ const node = readThrough(written);
1341
1638
  if (node.type !== "identifier" && node.type !== "constant") {
1342
1639
  return nodeId(filePath, node);
1343
1640
  }
@@ -1345,10 +1642,7 @@ function readKey(filePath, node, enclosing) {
1345
1642
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1346
1643
  return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1347
1644
  }
1348
- if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1349
- return `${nodeId(filePath, enclosing)}#${node.text}`;
1350
- }
1351
- return nameId(filePath, node.text);
1645
+ return nameKey(filePath, node, enclosing);
1352
1646
  }
1353
1647
  var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1354
1648
  "string",
@@ -1377,15 +1671,12 @@ function children2(node) {
1377
1671
  function add(emitter, relation, ...tuple) {
1378
1672
  emitter.db.add(relation, tuple);
1379
1673
  }
1380
- function valueKey(emitter, value) {
1674
+ function valueKey(emitter, written) {
1675
+ const value = readThrough(written);
1381
1676
  if (value.type !== "identifier" && value.type !== "constant") {
1382
1677
  return nodeId(emitter.filePath, value);
1383
1678
  }
1384
- const enclosing = emitter.enclosing;
1385
- if (enclosing !== null && enclosing.params.has(value.text)) {
1386
- return `${enclosing.funcKey}#${value.text}`;
1387
- }
1388
- return nameId(emitter.filePath, value.text);
1679
+ return nameKey(emitter.filePath, value, emitter.enclosing);
1389
1680
  }
1390
1681
  function pairKeyText(key) {
1391
1682
  if (key.type === "hash_key_symbol") {
@@ -1524,8 +1815,72 @@ function emitExpressionFacts(emitter, node) {
1524
1815
  if (child.type === "nil") {
1525
1816
  add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
1526
1817
  }
1818
+ if (child.type === "self" && emitter.selfKey !== null) {
1819
+ add(emitter, "binds", nodeId(emitter.filePath, child), emitter.selfKey);
1820
+ }
1821
+ if (child.type === "instance_variable") {
1822
+ emitInstanceRead(emitter, child);
1823
+ }
1824
+ if (ASSIGNMENT_TYPES.has(child.type)) {
1825
+ collectInstanceWrite(emitter, child);
1826
+ }
1527
1827
  });
1528
1828
  }
1829
+ var ASSIGNMENT_TYPES = /* @__PURE__ */ new Set(["assignment", "operator_assignment"]);
1830
+ function isWriteTarget(node) {
1831
+ const parent = node.parent;
1832
+ return parent !== null && ASSIGNMENT_TYPES.has(parent.type) && field(parent, "left")?.id === node.id;
1833
+ }
1834
+ function emitInstanceRead(emitter, node) {
1835
+ if (emitter.selfKey === null || isWriteTarget(node)) {
1836
+ return;
1837
+ }
1838
+ add(
1839
+ emitter,
1840
+ "readsProperty",
1841
+ nodeId(emitter.filePath, node),
1842
+ emitter.selfKey,
1843
+ node.text
1844
+ );
1845
+ }
1846
+ function collectInstanceWrite(emitter, node) {
1847
+ const left = field(node, "left");
1848
+ const right = field(node, "right");
1849
+ const collector = emitter.instanceWrites;
1850
+ if (left === null || right === null || collector === null) {
1851
+ return;
1852
+ }
1853
+ if (left.type !== "instance_variable") {
1854
+ return;
1855
+ }
1856
+ const operator = field(node, "operator")?.text;
1857
+ const value = node.type === "assignment" || WHOLE_VALUE_OPERATORS.has(operator ?? "") ? right : null;
1858
+ const written = collector.get(left.text) ?? [];
1859
+ written.push(
1860
+ describeWrite(emitter, {
1861
+ name: left.text,
1862
+ target: left,
1863
+ value,
1864
+ at: node,
1865
+ fromParameter: false
1866
+ })
1867
+ );
1868
+ collector.set(left.text, written);
1869
+ }
1870
+ function emitInstanceWrites(emitter, classKey, collected) {
1871
+ for (const [name, writes] of collected) {
1872
+ const settled = (0, import_resolution3.valueLeftByWrites)(writes, false);
1873
+ if (settled !== null) {
1874
+ add(emitter, "holdsProperty", classKey, name, settled);
1875
+ continue;
1876
+ }
1877
+ for (const write of writes) {
1878
+ if (write.value !== null && !write.narrowsName) {
1879
+ add(emitter, "holdsProperty", classKey, name, write.value);
1880
+ }
1881
+ }
1882
+ }
1883
+ }
1529
1884
  function implicitReturn(body) {
1530
1885
  const statements = children2(body).filter(
1531
1886
  (child) => child.type !== "rescue" && child.type !== "ensure"
@@ -1536,23 +1891,17 @@ function implicitReturn(body) {
1536
1891
  function emitMethodFacts(emitter, method) {
1537
1892
  const funcKey = nodeId(emitter.filePath, method);
1538
1893
  add(emitter, "func", funcKey);
1539
- const params = field(method, "parameters");
1540
- const declared = /* @__PURE__ */ new Set();
1541
1894
  let position = 0;
1542
- for (const param of params === null ? [] : children2(params)) {
1895
+ for (const param of parametersOf(method)) {
1543
1896
  const paramName = paramNameOf(param);
1544
1897
  if (paramName !== null) {
1545
- declared.add(paramName.text);
1546
1898
  const paramKey = `${funcKey}#${paramName.text}`;
1547
1899
  add(emitter, "paramOf", funcKey, String(position), paramKey);
1548
1900
  add(emitter, "paramNamed", funcKey, paramName.text, paramKey);
1549
1901
  }
1550
1902
  position += 1;
1551
1903
  }
1552
- const inside = {
1553
- ...emitter,
1554
- enclosing: { funcKey, params: declared }
1555
- };
1904
+ const inside = { ...emitter, enclosing: method };
1556
1905
  const body = field(method, "body");
1557
1906
  if (body === null) {
1558
1907
  return funcKey;
@@ -1584,31 +1933,78 @@ function emitMethodFacts(emitter, method) {
1584
1933
  add(inside, "returnsValue", funcKey, valueKey(inside, implicit));
1585
1934
  }
1586
1935
  emitExpressionFacts(inside, body);
1936
+ emitScopeWrites(inside, method, body);
1587
1937
  return funcKey;
1588
1938
  }
1589
- function emitAssignment(emitter, assignment2) {
1590
- const left = field(assignment2, "left");
1591
- const right = field(assignment2, "right");
1592
- if (right === null) {
1593
- return;
1939
+ function isConstruction(value) {
1940
+ return value.type === "call" || ARRAY_TYPES.has(value.type) || value.type === "hash";
1941
+ }
1942
+ function sourceOf(node) {
1943
+ return node.text.replace(/\s+/g, " ").trim();
1944
+ }
1945
+ function readFirst(node) {
1946
+ const inner = readThrough(node);
1947
+ if (inner !== node) {
1948
+ return inner;
1594
1949
  }
1595
- if (left?.type !== "identifier" && left?.type !== "constant") {
1596
- return;
1950
+ return node.type === "call" ? field(node, "receiver") : null;
1951
+ }
1952
+ var CHAIN_READS = {
1953
+ nameTypes: RUBY_NAME_TYPES,
1954
+ readFirst
1955
+ };
1956
+ function describeWrite(emitter, write) {
1957
+ const value = write.value === null ? null : readThrough(write.value);
1958
+ return {
1959
+ value: value === null ? null : valueKey(emitter, value),
1960
+ placeholder: value?.type === "nil",
1961
+ construction: value !== null && isConstruction(value) ? sourceOf(value) : null,
1962
+ narrowsName: value !== null && value.type === "call" && (0, import_resolution3.startsAtName)(value, write.name, CHAIN_READS)
1963
+ };
1964
+ }
1965
+ function settledValue(emitter, group) {
1966
+ const only = group.writes.length === 1 ? group.writes[0] : void 0;
1967
+ if (only !== void 0) {
1968
+ return only.fromParameter || only.value === null ? null : valueKey(emitter, only.value);
1597
1969
  }
1598
- add(
1599
- emitter,
1600
- "binds",
1601
- nameId(emitter.filePath, left.text),
1602
- valueKey(emitter, right)
1603
- );
1604
- add(
1605
- emitter,
1606
- "exportsAs",
1607
- emitter.filePath,
1608
- left.text,
1609
- valueKey(emitter, right)
1970
+ return (0, import_resolution3.valueLeftByWrites)(
1971
+ group.writes.map((write) => describeWrite(emitter, write)),
1972
+ group.ordered
1610
1973
  );
1611
1974
  }
1975
+ function emitScopeWrites(emitter, method, body) {
1976
+ for (const group of collectWrites(method, body)) {
1977
+ const settled = settledValue(emitter, group);
1978
+ const key = group.owner === null ? nameId(emitter.filePath, group.name) : `${nodeId(emitter.filePath, group.owner)}#${group.name}`;
1979
+ if (settled === null) {
1980
+ emitCandidates(emitter, key, group);
1981
+ continue;
1982
+ }
1983
+ add(
1984
+ emitter,
1985
+ group.writes.length === 1 ? "binds" : "endsHolding",
1986
+ key,
1987
+ settled
1988
+ );
1989
+ if (group.owner === null) {
1990
+ add(emitter, "exportsAs", emitter.filePath, group.name, settled);
1991
+ }
1992
+ }
1993
+ }
1994
+ function emitCandidates(emitter, key, group) {
1995
+ for (const write of group.writes) {
1996
+ if (write.value === null) {
1997
+ add(emitter, "writesUnstated", key);
1998
+ continue;
1999
+ }
2000
+ if (write.fromParameter) {
2001
+ continue;
2002
+ }
2003
+ if (!describeWrite(emitter, write).narrowsName) {
2004
+ add(emitter, "mayHold", key, valueKey(emitter, write.value));
2005
+ }
2006
+ }
2007
+ }
1612
2008
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1613
2009
  function emitClassFacts(emitter, cls) {
1614
2010
  const classKey = nodeId(emitter.filePath, cls);
@@ -1620,6 +2016,12 @@ function emitClassFacts(emitter, cls) {
1620
2016
  add(emitter, "extendsNamed", classKey, base.text);
1621
2017
  }
1622
2018
  const body = field(cls, "body");
2019
+ const collected = /* @__PURE__ */ new Map();
2020
+ const within = {
2021
+ ...emitter,
2022
+ selfKey: classKey,
2023
+ instanceWrites: collected
2024
+ };
1623
2025
  for (const statement of body === null ? [] : children2(body)) {
1624
2026
  if (statement.type === "assignment") {
1625
2027
  const left = field(statement, "left");
@@ -1643,16 +2045,23 @@ function emitClassFacts(emitter, cls) {
1643
2045
  if (!METHOD_TYPES.has(statement.type)) {
1644
2046
  continue;
1645
2047
  }
1646
- const funcKey = emitMethodFacts(emitter, statement);
2048
+ const funcKey = emitMethodFacts(within, statement);
1647
2049
  const name = field(statement, "name");
1648
2050
  if (name !== null) {
1649
2051
  add(emitter, "holdsProperty", classKey, name.text, funcKey);
1650
2052
  }
1651
2053
  }
2054
+ emitInstanceWrites(within, classKey, collected);
1652
2055
  return classKey;
1653
2056
  }
1654
2057
  function emitValueFacts(db, filePath, root) {
1655
- const emitter = { db, filePath, enclosing: null };
2058
+ const emitter = {
2059
+ db,
2060
+ filePath,
2061
+ enclosing: null,
2062
+ selfKey: null,
2063
+ instanceWrites: null
2064
+ };
1656
2065
  const declaresName = (child, key) => {
1657
2066
  const name = field(child, "name");
1658
2067
  if (name === null) {
@@ -1670,14 +2079,12 @@ function emitValueFacts(db, filePath, root) {
1670
2079
  if (METHOD_TYPES.has(child.type)) {
1671
2080
  declaresName(child, emitMethodFacts(emitter, child));
1672
2081
  }
1673
- if (child.type === "assignment") {
1674
- emitAssignment(emitter, child);
1675
- }
1676
2082
  walk(child);
1677
2083
  }
1678
2084
  };
1679
2085
  walk(root);
1680
2086
  emitExpressionFacts(emitter, root);
2087
+ emitScopeWrites(emitter, null, root);
1681
2088
  }
1682
2089
 
1683
2090
  // src/values/lowering.ts
@@ -1693,7 +2100,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1693
2100
  "module",
1694
2101
  "singleton_class"
1695
2102
  ]);
1696
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
2103
+ var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1697
2104
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1698
2105
  var NAME_TYPES = /* @__PURE__ */ new Set([
1699
2106
  "identifier",
@@ -2199,7 +2606,7 @@ function isTailExpression(node) {
2199
2606
  if (owner.type === "block" && owner.parent?.type === "lambda") {
2200
2607
  return true;
2201
2608
  }
2202
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
2609
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES3.has(owner.type)) {
2203
2610
  return true;
2204
2611
  }
2205
2612
  if (owner.type === "when") {
@@ -2218,7 +2625,7 @@ function isRoot(node) {
2218
2625
  if (OWN_ROOT_TYPES.has(node.type)) {
2219
2626
  return true;
2220
2627
  }
2221
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
2628
+ return BLOCK_TYPES3.has(node.type) && node.parent?.type === "call";
2222
2629
  }
2223
2630
  function isPathStatement(node, parent) {
2224
2631
  if (node.type === "elsif") {
@@ -2246,7 +2653,7 @@ function siteOf(node) {
2246
2653
  current = parent;
2247
2654
  }
2248
2655
  }
2249
- function parametersOf(parameters) {
2656
+ function parametersOf2(parameters) {
2250
2657
  if (parameters === null) {
2251
2658
  return [];
2252
2659
  }
@@ -2268,19 +2675,19 @@ function functionOf(node) {
2268
2675
  return null;
2269
2676
  }
2270
2677
  return {
2271
- parameters: parametersOf(field(node, "parameters")),
2678
+ parameters: parametersOf2(field(node, "parameters")),
2272
2679
  body: bodyOf(block)
2273
2680
  };
2274
2681
  }
2275
- if (BLOCK_TYPES2.has(node.type)) {
2682
+ if (BLOCK_TYPES3.has(node.type)) {
2276
2683
  return {
2277
- parameters: parametersOf(field(node, "parameters")),
2684
+ parameters: parametersOf2(field(node, "parameters")),
2278
2685
  body: bodyOf(node)
2279
2686
  };
2280
2687
  }
2281
2688
  if (METHOD_TYPES2.has(node.type)) {
2282
2689
  const body = field(node, "body");
2283
- const parameters = parametersOf(field(node, "parameters"));
2690
+ const parameters = parametersOf2(field(node, "parameters"));
2284
2691
  if (body === null) {
2285
2692
  return { parameters, body: [] };
2286
2693
  }
@@ -2623,11 +3030,18 @@ function bindEvaluator(db, nodes) {
2623
3030
  )
2624
3031
  );
2625
3032
  }
2626
- function evaluatedValue(node, db) {
2627
- return (0, import_values2.force)(evaluatorFor(node, db).evaluate(node));
3033
+ function evaluatedValue(node, db, bindings) {
3034
+ const evaluator = evaluatorFor(node, db);
3035
+ if (bindings === void 0) {
3036
+ return (0, import_values2.force)(evaluator.evaluate(node));
3037
+ }
3038
+ const supplied = new Map(
3039
+ [...bindings].map(([name, value]) => [name, (0, import_values2.text)(value)])
3040
+ );
3041
+ return (0, import_values2.force)(evaluator.evaluate(node, { bindings: supplied }));
2628
3042
  }
2629
- function stringValueOf(node, db) {
2630
- return (0, import_values2.literalOf)(evaluatedValue(node, db));
3043
+ function stringValueOf(node, db, bindings) {
3044
+ return (0, import_values2.literalOf)(evaluatedValue(node, db, bindings));
2631
3045
  }
2632
3046
  function methodDefinitionsIn(file, root) {
2633
3047
  const found = /* @__PURE__ */ new Map();
@@ -2685,7 +3099,7 @@ function contextOver(db, nodes) {
2685
3099
  db,
2686
3100
  readKey(file, node, enclosingMethod(node))
2687
3101
  );
2688
- return answer === null ? null : (0, import_resolution2.nodeOfKey)(rootsByFile, answer);
3102
+ return answer === null ? null : (0, import_resolution4.nodeOfKey)(rootsByFile, answer);
2689
3103
  },
2690
3104
  callable: (call) => {
2691
3105
  const file = fileOf(call);
@@ -3288,8 +3702,7 @@ function controllerFilters(pattern, ancestry) {
3288
3702
  skipNames.set(form.skippedBy, form.name);
3289
3703
  }
3290
3704
  }
3291
- const declared = [];
3292
- const skips = [];
3705
+ let declared = [];
3293
3706
  for (const { statement } of inheritedStatements(ancestry)) {
3294
3707
  const called = calledName(statement);
3295
3708
  if (called === null) {
@@ -3297,16 +3710,21 @@ function controllerFilters(pattern, ancestry) {
3297
3710
  }
3298
3711
  const form = byName.get(called);
3299
3712
  if (form !== void 0) {
3300
- declared.push(...declarationsOf(statement, form));
3713
+ for (const declaration of declarationsOf(statement, form)) {
3714
+ declared = [
3715
+ ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
3716
+ declaration
3717
+ ];
3718
+ }
3301
3719
  continue;
3302
3720
  }
3303
3721
  const skipped = skipNames.get(called);
3304
3722
  if (skipped !== void 0) {
3305
- skips.push(...skipsOf(statement, skipped));
3723
+ declared = applySkips(declared, skipsOf(statement, skipped));
3306
3724
  }
3307
3725
  }
3308
3726
  const resolved = [];
3309
- for (const declaration of applySkips(declared, skips)) {
3727
+ for (const declaration of declared) {
3310
3728
  const found = methodInAncestry(ancestry, declaration.methodName);
3311
3729
  if (found.type !== "found") {
3312
3730
  continue;
@@ -3419,6 +3837,9 @@ function skipsOf(statement, filterName) {
3419
3837
  const actions = namesUnder(args.keyword.only);
3420
3838
  return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3421
3839
  }
3840
+ function sameFilter(one, other) {
3841
+ return one.filter.name === other.filter.name && one.methodName === other.methodName;
3842
+ }
3422
3843
  function applySkips(declared, skips) {
3423
3844
  const left = [];
3424
3845
  for (const declaration of declared) {
@@ -3461,9 +3882,8 @@ function namesUnder(node) {
3461
3882
  }
3462
3883
 
3463
3884
  // src/storage.ts
3464
- var import_datalog2 = require("@suss/datalog");
3465
3885
  var import_ir_core = require("@suss/ir-core");
3466
- var import_resolution3 = require("@suss/resolution");
3886
+ var import_resolution5 = require("@suss/resolution");
3467
3887
  function children3(node) {
3468
3888
  return node.namedChildren.filter((child) => child !== null);
3469
3889
  }
@@ -3489,13 +3909,8 @@ function isConstant(node) {
3489
3909
  function constantName(constant2) {
3490
3910
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
3491
3911
  }
3492
- var ANCESTRY_PROGRAM = (0, import_datalog2.deriveOnDemand)(
3493
- [...import_resolution3.RESOLUTION_RULES, ...import_resolution3.RESOLUTION_QUESTIONS],
3494
- import_resolution3.ANSWER_RELATIONS
3495
- );
3496
3912
  function reachesBase(facts, classKey, bases) {
3497
- facts.add("wantedAncestry", [classKey]);
3498
- (0, import_datalog2.evaluate)(facts, ANCESTRY_PROGRAM.rules);
3913
+ (0, import_resolution5.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
3499
3914
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
3500
3915
  }
3501
3916
  function classBehind(facts, file, constant2) {
@@ -3542,7 +3957,7 @@ function storageEffect(call, model, pattern, kind, selector) {
3542
3957
  binding: (0, import_ir_core.storageBinding)({
3543
3958
  recognition: "ruby-storage",
3544
3959
  storageSystem: pattern.storageSystem,
3545
- scope: pattern.baseClasses[0] ?? "",
3960
+ scope: "default",
3546
3961
  container: constantName(model)
3547
3962
  }),
3548
3963
  callee: call.text,
@@ -3710,6 +4125,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
3710
4125
  lookup: {
3711
4126
  root: pattern.root,
3712
4127
  pathConvention: pattern.pathConvention,
4128
+ acronyms: pattern.acronyms ?? [],
3713
4129
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3714
4130
  parsedFile: (absPath) => cache.get(absPath),
3715
4131
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
@@ -3824,12 +4240,16 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3824
4240
  const lookup = {
3825
4241
  root: pattern.root,
3826
4242
  pathConvention: pattern.pathConvention,
4243
+ acronyms: pattern.acronyms ?? [],
3827
4244
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3828
4245
  parsedFile: (absPath) => options.cache.get(absPath),
3829
4246
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
3830
4247
  };
3831
4248
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
3832
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
4249
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, [
4250
+ ...pattern.baseClassNames,
4251
+ ...pattern.ancestryRootClassNames
4252
+ ])) {
3833
4253
  return [];
3834
4254
  }
3835
4255
  const filters = controllerFilters(pattern, ancestry);
@@ -3849,43 +4269,73 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3849
4269
  enclosingQualifiedName: filter.enclosingQualifiedName
3850
4270
  });
3851
4271
  }
3852
- for (const block of ownBlocks) {
3853
- if (block.info.bodyNode === null) {
4272
+ const emitAction = (actionName, method, block) => {
4273
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
4274
+ (filter) => filterReference(
4275
+ filter,
4276
+ options.displayPathOf?.(filter.file) ?? filter.file
4277
+ )
4278
+ );
4279
+ const raw = buildControllerActionUnit(
4280
+ pack,
4281
+ pattern,
4282
+ info.qualifiedName,
4283
+ actionName,
4284
+ method,
4285
+ {
4286
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
4287
+ absolute: block.file
4288
+ },
4289
+ options,
4290
+ around
4291
+ );
4292
+ units.push(raw);
4293
+ options.onReachSeed?.(raw, {
4294
+ file: block.file,
4295
+ node: method,
4296
+ enclosingQualifiedName: block.info.qualifiedName
4297
+ });
4298
+ };
4299
+ const own = /* @__PURE__ */ new Set();
4300
+ for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
4301
+ own.add(actionName);
4302
+ emitAction(actionName, method, block);
4303
+ }
4304
+ const seen = new Set(own);
4305
+ for (const entry of ancestry) {
4306
+ if (entry.type !== "bodies" || entry.name === info.qualifiedName) {
3854
4307
  continue;
3855
4308
  }
3856
- const visibility = instanceMethodVisibility(block.info.bodyNode);
3857
- for (const [actionName, method] of instanceMethodsByName(
3858
- block.info.bodyNode
4309
+ for (const [actionName, method, block] of publicInstanceMethods(
4310
+ entry.blocks
3859
4311
  )) {
3860
- if ((visibility.get(actionName) ?? "public") !== "public") {
4312
+ if (seen.has(actionName)) {
3861
4313
  continue;
3862
4314
  }
3863
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3864
- (filter) => filterReference(
3865
- filter,
3866
- options.displayPathOf?.(filter.file) ?? filter.file
3867
- )
3868
- );
3869
- const raw = buildControllerActionUnit(
3870
- pack,
3871
- pattern,
3872
- info.qualifiedName,
3873
- actionName,
3874
- method,
3875
- { display: options.filePath, absolute: block.file },
3876
- options,
3877
- around
3878
- );
3879
- units.push(raw);
3880
- options.onReachSeed?.(raw, {
3881
- file: block.file,
3882
- node: method,
3883
- enclosingQualifiedName: info.qualifiedName
3884
- });
4315
+ seen.add(actionName);
4316
+ if (pattern.routeFor(info.qualifiedName, actionName) === null) {
4317
+ continue;
4318
+ }
4319
+ emitAction(actionName, method, block);
3885
4320
  }
3886
4321
  }
3887
4322
  return units;
3888
4323
  }
4324
+ function publicInstanceMethods(blocks) {
4325
+ const found = [];
4326
+ for (const block of blocks) {
4327
+ if (block.info.bodyNode === null) {
4328
+ continue;
4329
+ }
4330
+ const visibility = instanceMethodVisibility(block.info.bodyNode);
4331
+ for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
4332
+ if ((visibility.get(name) ?? "public") === "public") {
4333
+ found.push([name, method, block]);
4334
+ }
4335
+ }
4336
+ }
4337
+ return found;
4338
+ }
3889
4339
  function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
3890
4340
  const range = rangeOf(method);
3891
4341
  const route = pattern.routeFor(controllerQualifiedName, actionName);
@@ -4398,7 +4848,8 @@ function collectFileConstants(filePath, root) {
4398
4848
  if (written !== null) {
4399
4849
  definitions.push({
4400
4850
  qualifiedName: qualify(nesting, written),
4401
- key: nodeId(filePath, node)
4851
+ key: nodeId(filePath, node),
4852
+ kind: "declaration"
4402
4853
  });
4403
4854
  const inside = [...nesting, ...written.split("::")];
4404
4855
  for (const child of children4(node)) {
@@ -4413,7 +4864,8 @@ function collectFileConstants(filePath, root) {
4413
4864
  if (left !== null && left.type === "constant" && right !== null) {
4414
4865
  definitions.push({
4415
4866
  qualifiedName: qualify(nesting, left.text),
4416
- key: nodeId(filePath, right)
4867
+ key: nodeId(filePath, right),
4868
+ kind: "assignment"
4417
4869
  });
4418
4870
  }
4419
4871
  }
@@ -4440,14 +4892,18 @@ function emitConstantBindings(db, perFile) {
4440
4892
  for (const file of files) {
4441
4893
  for (const definition of file.definitions) {
4442
4894
  const found = byName.get(definition.qualifiedName) ?? [];
4443
- found.push(definition.key);
4895
+ found.push(definition);
4444
4896
  byName.set(definition.qualifiedName, found);
4445
4897
  fileOfDefinition.set(definition.key, file.filePath);
4446
4898
  }
4447
4899
  }
4448
4900
  const settles = (candidate) => {
4449
- const found = byName.get(candidate);
4450
- return found !== void 0 && found.length === 1 ? found[0] ?? null : null;
4901
+ const found = byName.get(candidate) ?? [];
4902
+ const declared = found.filter((one) => one.kind === "declaration");
4903
+ if (declared.length > 0) {
4904
+ return declared[0].key;
4905
+ }
4906
+ return found.length === 1 ? found[0].key : null;
4451
4907
  };
4452
4908
  for (const file of files) {
4453
4909
  for (const reference of file.references) {
@@ -4473,8 +4929,8 @@ function emitConstantBindings(db, perFile) {
4473
4929
  // src/facts/why.ts
4474
4930
  var import_node_fs4 = __toESM(require("fs"), 1);
4475
4931
  var import_node_path5 = __toESM(require("path"), 1);
4476
- var import_datalog5 = require("@suss/datalog");
4477
- var import_resolution4 = require("@suss/resolution");
4932
+ var import_datalog4 = require("@suss/datalog");
4933
+ var import_resolution7 = require("@suss/resolution");
4478
4934
 
4479
4935
  // src/parser.ts
4480
4936
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -4536,7 +4992,7 @@ function parseWith(language, source) {
4536
4992
  var import_node_fs3 = __toESM(require("fs"), 1);
4537
4993
  var import_node_path4 = __toESM(require("path"), 1);
4538
4994
  var import_behavioral_ir5 = require("@suss/behavioral-ir");
4539
- var import_datalog4 = require("@suss/datalog");
4995
+ var import_datalog3 = require("@suss/datalog");
4540
4996
  var import_extractor10 = require("@suss/extractor");
4541
4997
 
4542
4998
  // src/diagnostics.ts
@@ -4591,34 +5047,118 @@ function requiredRelativePath(node) {
4591
5047
 
4592
5048
  // src/reach/closure.ts
4593
5049
  var import_behavioral_ir4 = require("@suss/behavioral-ir");
4594
- var import_datalog3 = require("@suss/datalog");
5050
+ var import_datalog2 = require("@suss/datalog");
4595
5051
  var import_extractor8 = require("@suss/extractor");
4596
5052
 
4597
5053
  // src/reach/resolveCallee.ts
5054
+ var import_resolution6 = require("@suss/resolution");
4598
5055
  var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
5056
+ var INVOKES_RECEIVER = "call";
5057
+ var STOP_FOR = {
5058
+ severalSources: "multipleSources",
5059
+ outsideRun: "outsideRun",
5060
+ unsettled: "unsettledValue",
5061
+ undeclared: "noDeclaration"
5062
+ };
4599
5063
  var stop = (reason) => ({
4600
5064
  kind: "stopped",
4601
5065
  reason
4602
5066
  });
5067
+ var NO_DECLARATION = stop("noDeclaration");
4603
5068
  function followed(target) {
4604
5069
  return { kind: "followed", target };
4605
5070
  }
4606
- async function resolveCallee(call, site, ctx) {
5071
+ function calleeSpellings(calls, ctx) {
5072
+ const spellingOf = /* @__PURE__ */ new Map();
5073
+ const keys = /* @__PURE__ */ new Set();
5074
+ for (const { call, site } of calls) {
5075
+ const spelling = spellingFor(call, site);
5076
+ spellingOf.set(call.id, spelling);
5077
+ if (spelling.kind === "receiver") {
5078
+ keys.add(spelling.key);
5079
+ }
5080
+ }
5081
+ return {
5082
+ spellingOf,
5083
+ outcomes: (0, import_resolution6.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
5084
+ };
5085
+ }
5086
+ async function resolveCallee(call, site, ctx, read) {
5087
+ const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
5088
+ if (spelling.kind === "stopped") {
5089
+ return spelling;
5090
+ }
5091
+ if (spelling.kind === "implicitSelf") {
5092
+ return resolveImplicitSelf(spelling.name, site, ctx);
5093
+ }
5094
+ const outcome = read?.outcomes.get(spelling.key) ?? (0, import_resolution6.calleeOutcomeOf)(ctx.facts, spelling.key, RUBY_PROGRAM);
5095
+ return asCallee(spelling, outcome, site, ctx);
5096
+ }
5097
+ function spellingFor(call, site) {
4607
5098
  const methodName = calleeMethodName(call);
4608
5099
  if (methodName === void 0) {
4609
- return stop("noDeclaration");
5100
+ return { kind: "stopped", reason: "noDeclaration" };
4610
5101
  }
4611
5102
  if (DYNAMIC_SEND_NAMES.has(methodName)) {
4612
- return stop("unsettledValue");
5103
+ return { kind: "stopped", reason: "unsettledValue" };
4613
5104
  }
4614
5105
  const receiver = field(call, "receiver");
4615
5106
  if (receiver === null || receiver.type === "self") {
4616
- return resolveImplicitSelf(methodName, site, ctx);
5107
+ return { kind: "implicitSelf", name: methodName };
5108
+ }
5109
+ return {
5110
+ kind: "receiver",
5111
+ key: readKey(site.file, receiver, site.method),
5112
+ method: methodName,
5113
+ onClassItself: receiver.type === "constant" || receiver.type === "scope_resolution"
5114
+ };
5115
+ }
5116
+ async function asCallee(spelling, outcome, site, ctx) {
5117
+ if (outcome.kind === "severalSources") {
5118
+ return stop("multipleSources");
5119
+ }
5120
+ if (outcome.kind === "callerSupplied") {
5121
+ return fromCaller(spelling, outcome.key, site);
5122
+ }
5123
+ const objects = objectsBehind(ctx.facts, spelling.key);
5124
+ if (objects.length > 1) {
5125
+ return stop("multipleSources");
5126
+ }
5127
+ const objectKey = objects[0] ?? (outcome.kind === "object" ? outcome.key : void 0);
5128
+ if (objectKey !== void 0) {
5129
+ return methodOnObject(spelling, objectKey, ctx);
5130
+ }
5131
+ if (outcome.kind === "function") {
5132
+ return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
5133
+ }
5134
+ return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
5135
+ }
5136
+ function objectsBehind(facts, key) {
5137
+ return [
5138
+ ...new Set(
5139
+ facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
5140
+ )
5141
+ ];
5142
+ }
5143
+ async function methodOnObject(spelling, objectKey, ctx) {
5144
+ const qualifiedName = ctx.classNames.get(objectKey);
5145
+ if (qualifiedName === void 0) {
5146
+ return NO_DECLARATION;
4617
5147
  }
4618
- if (receiver.type === "identifier" && methodName === "call" && site.ownParameters.includes(receiver.text)) {
4619
- return stop("callerSupplied");
5148
+ if (!spelling.onClassItself) {
5149
+ return methodOnAncestryOf(qualifiedName, spelling.method, ctx);
4620
5150
  }
4621
- return resolveOnReceiver(receiver, methodName, site, ctx);
5151
+ return spelling.method === "new" ? methodOnAncestryOf(qualifiedName, "initialize", ctx) : singletonMethodOn(qualifiedName, spelling.method, ctx);
5152
+ }
5153
+ function fromCaller(spelling, parameterKey, site) {
5154
+ if (!parameterKey.startsWith(`${site.owner}#`) || spelling.method !== INVOKES_RECEIVER) {
5155
+ return stop("unsettledValue");
5156
+ }
5157
+ return stop("callerSupplied");
5158
+ }
5159
+ function functionCallee(key, ctx) {
5160
+ const target = ctx.definitions.get(key);
5161
+ return target === void 0 ? NO_DECLARATION : followed(target);
4622
5162
  }
4623
5163
  async function resolveMethodReference(name, site, ctx) {
4624
5164
  const resolved = await resolveImplicitSelf(name, site, ctx);
@@ -4643,58 +5183,13 @@ async function resolveImplicitSelf(methodName, site, ctx) {
4643
5183
  function resolveTopLevelName(methodName, ctx) {
4644
5184
  const candidates = ctx.topLevelMethods.get(methodName);
4645
5185
  if (candidates === void 0 || candidates.length === 0) {
4646
- return stop("noDeclaration");
5186
+ return NO_DECLARATION;
4647
5187
  }
4648
5188
  if (candidates.length > 1) {
4649
5189
  return stop("multipleSources");
4650
5190
  }
4651
5191
  return followed(candidates[0]);
4652
5192
  }
4653
- async function resolveOnReceiver(receiver, methodName, site, ctx) {
4654
- if (receiver.type === "call") {
4655
- const innerName = calleeMethodName(receiver);
4656
- const innerReceiver = field(receiver, "receiver");
4657
- if (innerName === "new" && innerReceiver !== null) {
4658
- const classRef = classRefOf(innerReceiver, site, ctx);
4659
- return classRef === null ? stop("unsettledValue") : methodOnAncestryOf(classRef, methodName, ctx);
4660
- }
4661
- const root = rootConstantOf(receiver);
4662
- if (root === null) {
4663
- return stop("unsettledValue");
4664
- }
4665
- return classRefOf(root, site, ctx) === null ? stop("outsideRun") : stop("unsettledValue");
4666
- }
4667
- if (receiver.type === "constant" || receiver.type === "scope_resolution") {
4668
- const classRef = classRefOf(receiver, site, ctx);
4669
- return classRef === null ? stop("outsideRun") : singletonMethodOn(classRef, methodName, ctx);
4670
- }
4671
- return stop("unsettledValue");
4672
- }
4673
- function rootConstantOf(node) {
4674
- if (node.type === "constant" || node.type === "scope_resolution") {
4675
- return node;
4676
- }
4677
- if (node.type !== "call") {
4678
- return null;
4679
- }
4680
- const receiver = field(node, "receiver");
4681
- return receiver === null ? null : rootConstantOf(receiver);
4682
- }
4683
- function classRefOf(node, site, ctx) {
4684
- if (node.type === "constant") {
4685
- const shadow = shadowingClassFor(node, site.nesting, ctx.knownClasses);
4686
- if (shadow !== null) {
4687
- return shadow;
4688
- }
4689
- const qualified = qualifyConstantRef(node, site.nesting);
4690
- return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
4691
- }
4692
- if (node.type === "scope_resolution") {
4693
- const qualified = qualifyConstantRef(node, site.nesting);
4694
- return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
4695
- }
4696
- return null;
4697
- }
4698
5193
  async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
4699
5194
  const ownBlocks = ctx.lookup.localDefinition?.(qualifiedName) ?? null;
4700
5195
  if (ownBlocks === null) {
@@ -4710,7 +5205,7 @@ async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
4710
5205
  found.cause === "dynamicDefine" ? "unsettledValue" : "outsideRun"
4711
5206
  );
4712
5207
  }
4713
- return stop("noDeclaration");
5208
+ return NO_DECLARATION;
4714
5209
  }
4715
5210
  function reachedMethod(node, block, name) {
4716
5211
  return {
@@ -4735,21 +5230,21 @@ function singletonMethodOn(qualifiedName, methodName, ctx) {
4735
5230
  return followed(reachedMethod(found, block, methodName));
4736
5231
  }
4737
5232
  }
4738
- return stop("noDeclaration");
5233
+ return NO_DECLARATION;
4739
5234
  }
4740
5235
 
4741
5236
  // src/reach/closure.ts
4742
5237
  var REACHABLE_RULES = [
4743
- (0, import_datalog3.rule)("reachable", [(0, import_datalog3.variable)("f")], [(0, import_datalog3.lit)("entry", (0, import_datalog3.variable)("f"))]),
4744
- (0, import_datalog3.rule)(
5238
+ (0, import_datalog2.rule)("reachable", [(0, import_datalog2.variable)("f")], [(0, import_datalog2.lit)("entry", (0, import_datalog2.variable)("f"))]),
5239
+ (0, import_datalog2.rule)(
4745
5240
  "reachable",
4746
- [(0, import_datalog3.variable)("g")],
4747
- [(0, import_datalog3.lit)("reachable", (0, import_datalog3.variable)("f")), (0, import_datalog3.lit)("calls", (0, import_datalog3.variable)("f"), (0, import_datalog3.variable)("g"))]
5241
+ [(0, import_datalog2.variable)("g")],
5242
+ [(0, import_datalog2.lit)("reachable", (0, import_datalog2.variable)("f")), (0, import_datalog2.lit)("calls", (0, import_datalog2.variable)("f"), (0, import_datalog2.variable)("g"))]
4748
5243
  )
4749
5244
  ];
4750
5245
  async function reachedFunctions(seeds, options) {
4751
- const ctx = buildReachContext(options.files);
4752
- const db = new import_datalog3.Database();
5246
+ const ctx = buildReachContext(options.files, options.facts);
5247
+ const db = new import_datalog2.Database();
4753
5248
  const functionByKey = /* @__PURE__ */ new Map();
4754
5249
  const seedKeys = /* @__PURE__ */ new Set();
4755
5250
  const scanned = /* @__PURE__ */ new Set();
@@ -4770,18 +5265,26 @@ async function reachedFunctions(seeds, options) {
4770
5265
  db.add("entry", [seed.key]);
4771
5266
  }
4772
5267
  for (; ; ) {
4773
- (0, import_datalog3.evaluate)(db, REACHABLE_RULES);
5268
+ (0, import_datalog2.evaluate)(db, REACHABLE_RULES);
4774
5269
  const frontier = db.facts("reachable").map(([key]) => String(key)).filter((key) => !scanned.has(key));
4775
5270
  if (frontier.length === 0) {
4776
5271
  break;
4777
5272
  }
4778
- for (const key of frontier) {
5273
+ const bodies = frontier.flatMap((key) => {
4779
5274
  scanned.add(key);
4780
5275
  const source = functionByKey.get(key);
4781
- if (source === void 0) {
4782
- continue;
4783
- }
4784
- const scan = await scanBody(source, ctx, options);
5276
+ return source === void 0 ? [] : [{ key, source, ...bodyOf2(source, options) }];
5277
+ });
5278
+ const spellings = calleeSpellings(
5279
+ bodies.flatMap((body) => body.written),
5280
+ ctx
5281
+ );
5282
+ for (const { key, source, calls, site } of bodies) {
5283
+ const scan = await scanBody(source, ctx, options, {
5284
+ calls,
5285
+ site,
5286
+ spellings
5287
+ });
4785
5288
  if (scan.stops.length > 0) {
4786
5289
  stopsByKey.set(key, scan.stops);
4787
5290
  }
@@ -4834,11 +5337,22 @@ async function reachedFunctions(seeds, options) {
4834
5337
  function keyOf(target) {
4835
5338
  return nodeId(target.file, target.node);
4836
5339
  }
4837
- function buildReachContext(files) {
5340
+ var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5341
+ function buildReachContext(files, facts) {
4838
5342
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
4839
5343
  const classes = [];
5344
+ const classNames = /* @__PURE__ */ new Map();
5345
+ const classOfMethod = /* @__PURE__ */ new Map();
4840
5346
  for (const { file, root } of files) {
4841
- walkDefinitions(root, (info) => classes.push({ file, info }));
5347
+ walkDefinitions(root, (info) => {
5348
+ classes.push({ file, info });
5349
+ classNames.set(nodeId(file, info.node), info.qualifiedName);
5350
+ for (const statement of namedChildren(info.bodyNode)) {
5351
+ if (METHOD_TYPES4.has(statement.type)) {
5352
+ classOfMethod.set(nodeId(file, statement), info.qualifiedName);
5353
+ }
5354
+ }
5355
+ });
4842
5356
  }
4843
5357
  const knownClasses = new Set(classes.map(({ info }) => info.qualifiedName));
4844
5358
  for (const { file, info } of classes) {
@@ -4846,22 +5360,33 @@ function buildReachContext(files) {
4846
5360
  list.push({ info, knownClasses, file });
4847
5361
  blocksByQualifiedName.set(info.qualifiedName, list);
4848
5362
  }
4849
- const topLevelMethods = /* @__PURE__ */ new Map();
5363
+ const definitions = /* @__PURE__ */ new Map();
4850
5364
  for (const { file, root } of files) {
4851
- for (const method of topLevelMethodNodes(root)) {
5365
+ for (const [key, method] of methodDefinitionsIn(file, root)) {
4852
5366
  const name = field(method, "name")?.text;
4853
5367
  if (name === void 0) {
4854
5368
  continue;
4855
5369
  }
4856
- const list = topLevelMethods.get(name) ?? [];
4857
- list.push({
5370
+ const owner = classOfMethod.get(key) ?? null;
5371
+ definitions.set(key, {
4858
5372
  file,
4859
5373
  node: method,
4860
5374
  name,
4861
- exportPath: [name],
4862
- enclosingQualifiedName: null
5375
+ exportPath: owner === null ? [name] : [owner, name],
5376
+ enclosingQualifiedName: owner
4863
5377
  });
4864
- topLevelMethods.set(name, list);
5378
+ }
5379
+ }
5380
+ const topLevelMethods = /* @__PURE__ */ new Map();
5381
+ for (const { file, root } of files) {
5382
+ for (const method of topLevelMethodNodes(root)) {
5383
+ const found = definitions.get(nodeId(file, method));
5384
+ if (found === void 0) {
5385
+ continue;
5386
+ }
5387
+ const list = topLevelMethods.get(found.name) ?? [];
5388
+ list.push(found);
5389
+ topLevelMethods.set(found.name, list);
4865
5390
  }
4866
5391
  }
4867
5392
  const lookup = {
@@ -4874,7 +5399,13 @@ function buildReachContext(files) {
4874
5399
  parsedFile: async () => null,
4875
5400
  localDefinition: (name) => blocksByQualifiedName.get(name) ?? null
4876
5401
  };
4877
- return { lookup, knownClasses, topLevelMethods };
5402
+ return { lookup, topLevelMethods, facts, classNames, definitions };
5403
+ }
5404
+ function namedChildren(node) {
5405
+ if (node === null) {
5406
+ return [];
5407
+ }
5408
+ return node.namedChildren.filter((child) => child !== null);
4878
5409
  }
4879
5410
  function topLevelMethodNodes(root, found = []) {
4880
5411
  for (const child of bodyStatements(root)) {
@@ -4889,14 +5420,6 @@ function topLevelMethodNodes(root, found = []) {
4889
5420
  }
4890
5421
  return found;
4891
5422
  }
4892
- function nestingOf(qualifiedName) {
4893
- const parts = qualifiedName.split("::");
4894
- const out = [];
4895
- for (let depth = parts.length; depth >= 1; depth--) {
4896
- out.push(parts.slice(0, depth).join("::"));
4897
- }
4898
- return out;
4899
- }
4900
5423
  var EMPTY_SCAN = {
4901
5424
  followed: [],
4902
5425
  stops: [],
@@ -4914,21 +5437,23 @@ function methodReferenceSymbol(node) {
4914
5437
  const argChildren = args === null ? [] : bodyStatements(args);
4915
5438
  return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
4916
5439
  }
4917
- async function scanBody(source, ctx, options) {
5440
+ function bodyOf2(source, options) {
5441
+ const site = {
5442
+ file: source.file,
5443
+ method: source.node,
5444
+ owner: keyOf(source),
5445
+ enclosingQualifiedName: source.enclosingQualifiedName
5446
+ };
5447
+ const calls = field(source.node, "body") === null ? [] : withoutChainLinks(bodyCalls(source.node, options.inheritedMethods));
5448
+ return { calls, site, written: calls.map((call) => ({ call, site })) };
5449
+ }
5450
+ async function scanBody(source, ctx, options, read) {
4918
5451
  const displayPathOf = options.displayPathOf;
4919
- const body = field(source.node, "body");
4920
- if (body === null) {
5452
+ if (field(source.node, "body") === null) {
4921
5453
  return EMPTY_SCAN;
4922
5454
  }
4923
- const calls = withoutChainLinks(
4924
- bodyCalls(source.node, options.inheritedMethods)
4925
- );
5455
+ const { calls, site } = read;
4926
5456
  const ownParameters = positionalParameters(source.node).map((p) => p.name);
4927
- const site = {
4928
- enclosingQualifiedName: source.enclosingQualifiedName,
4929
- nesting: source.enclosingQualifiedName === null ? [] : nestingOf(source.enclosingQualifiedName),
4930
- ownParameters
4931
- };
4932
5457
  const followed2 = [];
4933
5458
  const stops = [];
4934
5459
  const placements = new import_behavioral_ir4.TargetPlacements();
@@ -4971,7 +5496,7 @@ async function scanBody(source, ctx, options) {
4971
5496
  };
4972
5497
  for (const call of calls) {
4973
5498
  const callee = calleeText(call);
4974
- const outcome = await resolveCallee(call, site, ctx);
5499
+ const outcome = await resolveCallee(call, site, ctx, read.spellings);
4975
5500
  const placed = outcome.kind === "followed" ? {
4976
5501
  file: displayPathOf(outcome.target.file),
4977
5502
  span: spanOf(outcome.target.node)
@@ -5109,7 +5634,8 @@ function inheritedMethodsIn(packs) {
5109
5634
  return found;
5110
5635
  }
5111
5636
  function alreadyDiscovered(seen, raw) {
5112
- const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
5637
+ const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
5638
+ const key = `${raw.identity.file}::${reported}::${raw.identity.range.start}`;
5113
5639
  if (seen.has(key)) {
5114
5640
  return true;
5115
5641
  }
@@ -5138,9 +5664,9 @@ async function extractRubyProject(options) {
5138
5664
  options.onCacheDiagnostic?.(lookup.diagnostic);
5139
5665
  if (lookup.kind === "hit") {
5140
5666
  options.onTiming?.(timer.report());
5141
- return { summaries: lookup.summaries, facts: new import_datalog4.Database() };
5667
+ return { summaries: lookup.summaries, facts: new import_datalog3.Database() };
5142
5668
  }
5143
- const db = new import_datalog4.Database();
5669
+ const db = new import_datalog3.Database();
5144
5670
  const summaries = [];
5145
5671
  const tallies = (0, import_extractor6.createPackTallies)(options.packs);
5146
5672
  const constants = [];
@@ -5269,6 +5795,7 @@ async function extractRubyProject(options) {
5269
5795
  () => reachedFunctions(seeds, {
5270
5796
  files: parsed,
5271
5797
  displayPathOf,
5798
+ facts: db,
5272
5799
  ...storage === void 0 ? {} : { storage },
5273
5800
  inheritedMethods
5274
5801
  })
@@ -5365,15 +5892,15 @@ function findRubyFiles(root) {
5365
5892
  }
5366
5893
 
5367
5894
  // src/facts/why.ts
5368
- var WITNESS_RULES = [...import_resolution4.RESOLUTION_RULES, ...RUBY_RULES];
5369
- var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5895
+ var WITNESS_RULES = [...import_resolution7.RESOLUTION_RULES, ...RUBY_RULES];
5896
+ var METHOD_TYPES5 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5370
5897
  function namedChildrenOf(node) {
5371
5898
  return node.namedChildren.filter((child) => child !== null);
5372
5899
  }
5373
5900
  function enclosingMethod2(node) {
5374
5901
  let current = node.parent;
5375
5902
  while (current !== null) {
5376
- if (METHOD_TYPES4.has(current.type)) {
5903
+ if (METHOD_TYPES5.has(current.type)) {
5377
5904
  return current;
5378
5905
  }
5379
5906
  current = current.parent;
@@ -5408,15 +5935,15 @@ function displayNameOf(node) {
5408
5935
  if (node.type === "class" || node.type === "module") {
5409
5936
  return field(node, "name")?.text ?? node.text;
5410
5937
  }
5411
- const text = node.text.split("\n")[0].trim();
5412
- return text.length > 40 ? `${text.slice(0, 40)}...` : text;
5938
+ const text2 = node.text.split("\n")[0].trim();
5939
+ return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
5413
5940
  }
5414
5941
  function width(node) {
5415
5942
  return node.endIndex - node.startIndex;
5416
5943
  }
5417
5944
  var RubyWhySession = class {
5418
5945
  root;
5419
- db = new import_datalog5.Database();
5946
+ db = new import_datalog4.Database();
5420
5947
  locations = /* @__PURE__ */ new Map();
5421
5948
  trees = /* @__PURE__ */ new Map();
5422
5949
  constructor(options) {
@@ -5436,14 +5963,14 @@ var RubyWhySession = class {
5436
5963
  * The smallest expression on `line` of `file` whose text is exactly
5437
5964
  * `text`, or null.
5438
5965
  */
5439
- findExpression(file, line, text) {
5966
+ findExpression(file, line, text2) {
5440
5967
  const root = this.rootOf(file);
5441
5968
  if (root === null) {
5442
5969
  return null;
5443
5970
  }
5444
5971
  let found = null;
5445
5972
  const visit = (node) => {
5446
- if (node.startPosition.row + 1 === line && node.text === text) {
5973
+ if (node.startPosition.row + 1 === line && node.text === text2) {
5447
5974
  if (found === null || width(node) <= width(found)) {
5448
5975
  found = node;
5449
5976
  }
@@ -5486,7 +6013,7 @@ var RubyWhySession = class {
5486
6013
  * which the caller says in its own words.
5487
6014
  */
5488
6015
  explain(value, options = {}) {
5489
- return (0, import_resolution4.explainResolvedKey)({
6016
+ return (0, import_resolution7.explainResolvedKey)({
5490
6017
  db: this.db,
5491
6018
  rules: WITNESS_RULES,
5492
6019
  key: readKey(value.file, value.node, enclosingMethod2(value.node)),
@@ -5614,6 +6141,7 @@ async function rubyStubEvidence(options) {
5614
6141
  inheritedStatements,
5615
6142
  methodDefinitionsIn,
5616
6143
  methodInAncestry,
6144
+ nestedStatements,
5617
6145
  nodeId,
5618
6146
  parseRuby,
5619
6147
  parseRubySync,