@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.js CHANGED
@@ -15,6 +15,45 @@ function field(node, name) {
15
15
  function bodyStatements(body) {
16
16
  return body.namedChildren.filter((child) => child !== null);
17
17
  }
18
+ var BRANCH_FIELDS = {
19
+ if: ["consequence", "alternative"],
20
+ unless: ["consequence", "alternative"],
21
+ elsif: ["consequence", "alternative"],
22
+ if_modifier: ["body"],
23
+ unless_modifier: ["body"],
24
+ while: ["body"],
25
+ until: ["body"],
26
+ while_modifier: ["body"],
27
+ until_modifier: ["body"],
28
+ when: ["body"],
29
+ rescue: ["body"]
30
+ };
31
+ var STATEMENT_LISTS = /* @__PURE__ */ new Set(["then", "else", "do", "ensure", "begin"]);
32
+ var CLAUSE_TYPES = /* @__PURE__ */ new Set(["elsif", "else", "when", "rescue", "ensure"]);
33
+ function nestedStatements(stmt) {
34
+ const branchFields = BRANCH_FIELDS[stmt.type];
35
+ if (branchFields !== void 0) {
36
+ return branchFields.flatMap((name) => {
37
+ const branch = field(stmt, name);
38
+ return branch === null ? [] : statementsRunBy(branch);
39
+ });
40
+ }
41
+ if (STATEMENT_LISTS.has(stmt.type)) {
42
+ return bodyStatements(stmt).flatMap(
43
+ (child) => CLAUSE_TYPES.has(child.type) ? nestedStatements(child) : [child]
44
+ );
45
+ }
46
+ if (stmt.type === "case") {
47
+ return bodyStatements(stmt).filter((child) => CLAUSE_TYPES.has(child.type)).flatMap(nestedStatements);
48
+ }
49
+ return [];
50
+ }
51
+ function statementsRunBy(branch) {
52
+ if (STATEMENT_LISTS.has(branch.type) || CLAUSE_TYPES.has(branch.type)) {
53
+ return nestedStatements(branch);
54
+ }
55
+ return [branch];
56
+ }
18
57
  var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
19
58
  "method",
20
59
  "singleton_method",
@@ -78,8 +117,8 @@ var VISIBILITY_KEYWORDS = /* @__PURE__ */ new Set([
78
117
  "protected",
79
118
  "public"
80
119
  ]);
81
- function visibilityKeyword(text) {
82
- return VISIBILITY_KEYWORDS.has(text) ? text : null;
120
+ function visibilityKeyword(text2) {
121
+ return VISIBILITY_KEYWORDS.has(text2) ? text2 : null;
83
122
  }
84
123
  function setVisibility(visibility, name, keyword) {
85
124
  if (keyword === "public") {
@@ -202,14 +241,31 @@ var NodeMap = class extends IdMap {
202
241
  // src/constantPath.ts
203
242
  import fs from "fs";
204
243
  import path from "path";
205
- function underscoreConstantPath(qualifiedName) {
206
- 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();
244
+ function underscoreConstantPath(qualifiedName, acronyms = []) {
245
+ 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();
246
+ }
247
+ function lowerAcronyms(word, acronyms) {
248
+ if (acronyms.length === 0) {
249
+ return word;
250
+ }
251
+ const alternatives = acronyms.map(escapeRegExp).join("|");
252
+ const acronymRegex = new RegExp(
253
+ `(?:(?<=([A-Za-z\\d]))|\\b)(${alternatives})(?=\\b|[^a-z])`,
254
+ "g"
255
+ );
256
+ return word.replace(
257
+ acronymRegex,
258
+ (_match, before, acronym) => `${before === void 0 ? "" : "_"}${acronym.toLowerCase()}`
259
+ );
260
+ }
261
+ function escapeRegExp(text2) {
262
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
207
263
  }
208
264
  var PATH_CONVENTIONS = {
209
265
  railsUnderscore: underscoreConstantPath
210
266
  };
211
- function resolveConstantFile(root, qualifiedName, convention) {
212
- const relative = `${PATH_CONVENTIONS[convention](qualifiedName)}.rb`;
267
+ function resolveConstantFile(root, qualifiedName, convention, acronyms = []) {
268
+ const relative = `${PATH_CONVENTIONS[convention](qualifiedName, acronyms)}.rb`;
213
269
  for (const candidate of [root, ...autoloadDirectories(root)]) {
214
270
  const file = path.join(candidate, relative);
215
271
  if (fs.existsSync(file)) {
@@ -283,12 +339,19 @@ function walkBody(statements, nesting, visit) {
283
339
  }
284
340
  }
285
341
  function ownIdentity(nameNode, outerNesting) {
286
- const qualifiedName = qualifyConstantRef(nameNode, outerNesting);
342
+ const qualifiedName = definedName(nameNode, outerNesting);
287
343
  if (qualifiedName === null) {
288
344
  return null;
289
345
  }
290
346
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
291
347
  }
348
+ function definedName(nameNode, outerNesting) {
349
+ const innermost = outerNesting[0] ?? null;
350
+ if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
351
+ return `${innermost}::${nameNode.text}`;
352
+ }
353
+ return qualifyConstantRef(nameNode, outerNesting);
354
+ }
292
355
  function visitClass(node, nesting, visit) {
293
356
  const nameNode = field(node, "name");
294
357
  const identity = nameNode !== null ? ownIdentity(nameNode, nesting) : null;
@@ -352,7 +415,8 @@ async function reachDefinition(qualifiedName, lookup) {
352
415
  const filePath = resolveConstantFile(
353
416
  lookup.root,
354
417
  qualifiedName,
355
- lookup.pathConvention
418
+ lookup.pathConvention,
419
+ lookup.acronyms
356
420
  );
357
421
  if (filePath === null) {
358
422
  return null;
@@ -394,7 +458,10 @@ async function chainOf(self, lookup, active) {
394
458
  async function superclassChain(self, lookup, active) {
395
459
  const candidates = superclassCandidatesOf(self.blocks);
396
460
  for (const candidate of candidates) {
397
- if (lookup.ancestryRootClassNames.includes(candidate) || active.has(candidate)) {
461
+ if (lookup.ancestryRootClassNames.includes(candidate)) {
462
+ return [{ type: "root", name: candidate }];
463
+ }
464
+ if (active.has(candidate)) {
398
465
  return [];
399
466
  }
400
467
  const blocks = await definitionOf(candidate, lookup);
@@ -472,6 +539,9 @@ function superclassCandidatesOf(blocks) {
472
539
  }
473
540
  function methodInAncestry(ancestry, name) {
474
541
  for (const entry of ancestry) {
542
+ if (entry.type === "root") {
543
+ return { type: "none" };
544
+ }
475
545
  if (entry.type === "unfollowed") {
476
546
  return {
477
547
  type: "unsettled",
@@ -1207,21 +1277,13 @@ function calledMethod(node) {
1207
1277
 
1208
1278
  // src/values/evaluator.ts
1209
1279
  import { nodeOfKey } from "@suss/resolution";
1210
- import { Evaluator, force, literalOf as literalOf2 } from "@suss/values";
1280
+ import { Evaluator, force, literalOf as literalOf2, text } from "@suss/values";
1211
1281
 
1212
1282
  // src/facts/resolve.ts
1283
+ import { constant, lit, rule, variable as v } from "@suss/datalog";
1213
1284
  import {
1214
- constant,
1215
- deriveOnDemand,
1216
- evaluate,
1217
- lit,
1218
- rule,
1219
- variable as v
1220
- } from "@suss/datalog";
1221
- import {
1222
- ANSWER_RELATIONS,
1223
- RESOLUTION_QUESTIONS,
1224
- RESOLUTION_RULES,
1285
+ askResolution,
1286
+ resolutionProgram,
1225
1287
  writtenValueOf as sharedWrittenValueOf,
1226
1288
  VALUE_STEP
1227
1289
  } from "@suss/resolution";
@@ -1239,18 +1301,9 @@ var RUBY_RULES = [
1239
1301
  ]
1240
1302
  )
1241
1303
  ];
1242
- var RESOLUTION_PROGRAM = deriveOnDemand(
1243
- [...RESOLUTION_RULES, ...RUBY_RULES, ...RESOLUTION_QUESTIONS],
1244
- ANSWER_RELATIONS
1245
- );
1304
+ var RUBY_PROGRAM = resolutionProgram(RUBY_RULES);
1246
1305
  function resolveValues(db, keys) {
1247
- if (keys.length === 0) {
1248
- return;
1249
- }
1250
- for (const key of keys) {
1251
- db.add("wanted", [key]);
1252
- }
1253
- evaluate(db, RESOLUTION_PROGRAM.rules);
1306
+ askResolution(db, keys, "wanted", RUBY_PROGRAM);
1254
1307
  }
1255
1308
  function resolvedFunctions(db, key) {
1256
1309
  return db.facts("wantedResolves").filter((row) => String(row[0]) === key).map((row) => String(row[1]));
@@ -1261,27 +1314,262 @@ function writtenValueOf(db, key) {
1261
1314
  }
1262
1315
 
1263
1316
  // src/facts/values.ts
1264
- function nodeId(filePath, node) {
1265
- return `${filePath}:${node.startIndex}-${node.endIndex}`;
1266
- }
1267
- function nameId(filePath, name) {
1268
- return `${filePath}#${name}`;
1269
- }
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
+ ]);
1270
1336
  function paramNameOf(param) {
1271
1337
  return param.type === "identifier" ? param : field(param, "name") ?? null;
1272
1338
  }
1273
- function declaredParamNames(method) {
1274
- const params = field(method, "parameters");
1275
- const declared = /* @__PURE__ */ new Set();
1276
- 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)) {
1277
1346
  const name = paramNameOf(param);
1278
1347
  if (name !== null) {
1279
- declared.add(name.text);
1348
+ names.add(name.text);
1280
1349
  }
1281
1350
  }
1282
- return declared;
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;
1283
1458
  }
1284
- function readKey(filePath, node, enclosing) {
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)) {
1488
+ const name = paramNameOf(param);
1489
+ if (name !== null) {
1490
+ found.push(writeOf(name, name, param, true));
1491
+ }
1492
+ }
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}`;
1570
+ }
1571
+ function readKey(filePath, written, enclosing) {
1572
+ const node = readThrough(written);
1285
1573
  if (node.type !== "identifier" && node.type !== "constant") {
1286
1574
  return nodeId(filePath, node);
1287
1575
  }
@@ -1289,10 +1577,7 @@ function readKey(filePath, node, enclosing) {
1289
1577
  if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
1290
1578
  return isPropertyRead2(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
1291
1579
  }
1292
- if (enclosing !== null && declaredParamNames(enclosing).has(node.text)) {
1293
- return `${nodeId(filePath, enclosing)}#${node.text}`;
1294
- }
1295
- return nameId(filePath, node.text);
1580
+ return nameKey(filePath, node, enclosing);
1296
1581
  }
1297
1582
  var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
1298
1583
  "string",
@@ -1321,15 +1606,12 @@ function children2(node) {
1321
1606
  function add(emitter, relation, ...tuple) {
1322
1607
  emitter.db.add(relation, tuple);
1323
1608
  }
1324
- function valueKey(emitter, value) {
1609
+ function valueKey(emitter, written) {
1610
+ const value = readThrough(written);
1325
1611
  if (value.type !== "identifier" && value.type !== "constant") {
1326
1612
  return nodeId(emitter.filePath, value);
1327
1613
  }
1328
- const enclosing = emitter.enclosing;
1329
- if (enclosing !== null && enclosing.params.has(value.text)) {
1330
- return `${enclosing.funcKey}#${value.text}`;
1331
- }
1332
- return nameId(emitter.filePath, value.text);
1614
+ return nameKey(emitter.filePath, value, emitter.enclosing);
1333
1615
  }
1334
1616
  function pairKeyText(key) {
1335
1617
  if (key.type === "hash_key_symbol") {
@@ -1468,8 +1750,72 @@ function emitExpressionFacts(emitter, node) {
1468
1750
  if (child.type === "nil") {
1469
1751
  add(emitter, "placeholderValue", nodeId(emitter.filePath, child));
1470
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
+ }
1471
1762
  });
1472
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
+ }
1473
1819
  function implicitReturn(body) {
1474
1820
  const statements = children2(body).filter(
1475
1821
  (child) => child.type !== "rescue" && child.type !== "ensure"
@@ -1480,23 +1826,17 @@ function implicitReturn(body) {
1480
1826
  function emitMethodFacts(emitter, method) {
1481
1827
  const funcKey = nodeId(emitter.filePath, method);
1482
1828
  add(emitter, "func", funcKey);
1483
- const params = field(method, "parameters");
1484
- const declared = /* @__PURE__ */ new Set();
1485
1829
  let position = 0;
1486
- for (const param of params === null ? [] : children2(params)) {
1830
+ for (const param of parametersOf(method)) {
1487
1831
  const paramName = paramNameOf(param);
1488
1832
  if (paramName !== null) {
1489
- declared.add(paramName.text);
1490
1833
  const paramKey = `${funcKey}#${paramName.text}`;
1491
1834
  add(emitter, "paramOf", funcKey, String(position), paramKey);
1492
1835
  add(emitter, "paramNamed", funcKey, paramName.text, paramKey);
1493
1836
  }
1494
1837
  position += 1;
1495
1838
  }
1496
- const inside = {
1497
- ...emitter,
1498
- enclosing: { funcKey, params: declared }
1499
- };
1839
+ const inside = { ...emitter, enclosing: method };
1500
1840
  const body = field(method, "body");
1501
1841
  if (body === null) {
1502
1842
  return funcKey;
@@ -1528,31 +1868,78 @@ function emitMethodFacts(emitter, method) {
1528
1868
  add(inside, "returnsValue", funcKey, valueKey(inside, implicit));
1529
1869
  }
1530
1870
  emitExpressionFacts(inside, body);
1871
+ emitScopeWrites(inside, method, body);
1531
1872
  return funcKey;
1532
1873
  }
1533
- function emitAssignment(emitter, assignment2) {
1534
- const left = field(assignment2, "left");
1535
- const right = field(assignment2, "right");
1536
- if (right === null) {
1537
- 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;
1538
1884
  }
1539
- if (left?.type !== "identifier" && left?.type !== "constant") {
1540
- 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);
1541
1904
  }
1542
- add(
1543
- emitter,
1544
- "binds",
1545
- nameId(emitter.filePath, left.text),
1546
- valueKey(emitter, right)
1547
- );
1548
- add(
1549
- emitter,
1550
- "exportsAs",
1551
- emitter.filePath,
1552
- left.text,
1553
- valueKey(emitter, right)
1905
+ return valueLeftByWrites(
1906
+ group.writes.map((write) => describeWrite(emitter, write)),
1907
+ group.ordered
1554
1908
  );
1555
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
+ }
1556
1943
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1557
1944
  function emitClassFacts(emitter, cls) {
1558
1945
  const classKey = nodeId(emitter.filePath, cls);
@@ -1564,6 +1951,12 @@ function emitClassFacts(emitter, cls) {
1564
1951
  add(emitter, "extendsNamed", classKey, base.text);
1565
1952
  }
1566
1953
  const body = field(cls, "body");
1954
+ const collected = /* @__PURE__ */ new Map();
1955
+ const within = {
1956
+ ...emitter,
1957
+ selfKey: classKey,
1958
+ instanceWrites: collected
1959
+ };
1567
1960
  for (const statement of body === null ? [] : children2(body)) {
1568
1961
  if (statement.type === "assignment") {
1569
1962
  const left = field(statement, "left");
@@ -1587,16 +1980,23 @@ function emitClassFacts(emitter, cls) {
1587
1980
  if (!METHOD_TYPES.has(statement.type)) {
1588
1981
  continue;
1589
1982
  }
1590
- const funcKey = emitMethodFacts(emitter, statement);
1983
+ const funcKey = emitMethodFacts(within, statement);
1591
1984
  const name = field(statement, "name");
1592
1985
  if (name !== null) {
1593
1986
  add(emitter, "holdsProperty", classKey, name.text, funcKey);
1594
1987
  }
1595
1988
  }
1989
+ emitInstanceWrites(within, classKey, collected);
1596
1990
  return classKey;
1597
1991
  }
1598
1992
  function emitValueFacts(db, filePath, root) {
1599
- const emitter = { db, filePath, enclosing: null };
1993
+ const emitter = {
1994
+ db,
1995
+ filePath,
1996
+ enclosing: null,
1997
+ selfKey: null,
1998
+ instanceWrites: null
1999
+ };
1600
2000
  const declaresName = (child, key) => {
1601
2001
  const name = field(child, "name");
1602
2002
  if (name === null) {
@@ -1614,14 +2014,12 @@ function emitValueFacts(db, filePath, root) {
1614
2014
  if (METHOD_TYPES.has(child.type)) {
1615
2015
  declaresName(child, emitMethodFacts(emitter, child));
1616
2016
  }
1617
- if (child.type === "assignment") {
1618
- emitAssignment(emitter, child);
1619
- }
1620
2017
  walk(child);
1621
2018
  }
1622
2019
  };
1623
2020
  walk(root);
1624
2021
  emitExpressionFacts(emitter, root);
2022
+ emitScopeWrites(emitter, null, root);
1625
2023
  }
1626
2024
 
1627
2025
  // src/values/lowering.ts
@@ -1637,7 +2035,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1637
2035
  "module",
1638
2036
  "singleton_class"
1639
2037
  ]);
1640
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
2038
+ var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1641
2039
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1642
2040
  var NAME_TYPES = /* @__PURE__ */ new Set([
1643
2041
  "identifier",
@@ -2143,7 +2541,7 @@ function isTailExpression(node) {
2143
2541
  if (owner.type === "block" && owner.parent?.type === "lambda") {
2144
2542
  return true;
2145
2543
  }
2146
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
2544
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES3.has(owner.type)) {
2147
2545
  return true;
2148
2546
  }
2149
2547
  if (owner.type === "when") {
@@ -2162,7 +2560,7 @@ function isRoot(node) {
2162
2560
  if (OWN_ROOT_TYPES.has(node.type)) {
2163
2561
  return true;
2164
2562
  }
2165
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
2563
+ return BLOCK_TYPES3.has(node.type) && node.parent?.type === "call";
2166
2564
  }
2167
2565
  function isPathStatement(node, parent) {
2168
2566
  if (node.type === "elsif") {
@@ -2190,7 +2588,7 @@ function siteOf(node) {
2190
2588
  current = parent;
2191
2589
  }
2192
2590
  }
2193
- function parametersOf(parameters) {
2591
+ function parametersOf2(parameters) {
2194
2592
  if (parameters === null) {
2195
2593
  return [];
2196
2594
  }
@@ -2212,19 +2610,19 @@ function functionOf(node) {
2212
2610
  return null;
2213
2611
  }
2214
2612
  return {
2215
- parameters: parametersOf(field(node, "parameters")),
2613
+ parameters: parametersOf2(field(node, "parameters")),
2216
2614
  body: bodyOf(block)
2217
2615
  };
2218
2616
  }
2219
- if (BLOCK_TYPES2.has(node.type)) {
2617
+ if (BLOCK_TYPES3.has(node.type)) {
2220
2618
  return {
2221
- parameters: parametersOf(field(node, "parameters")),
2619
+ parameters: parametersOf2(field(node, "parameters")),
2222
2620
  body: bodyOf(node)
2223
2621
  };
2224
2622
  }
2225
2623
  if (METHOD_TYPES2.has(node.type)) {
2226
2624
  const body = field(node, "body");
2227
- const parameters = parametersOf(field(node, "parameters"));
2625
+ const parameters = parametersOf2(field(node, "parameters"));
2228
2626
  if (body === null) {
2229
2627
  return { parameters, body: [] };
2230
2628
  }
@@ -2585,11 +2983,18 @@ function bindEvaluator(db, nodes) {
2585
2983
  )
2586
2984
  );
2587
2985
  }
2588
- function evaluatedValue(node, db) {
2589
- return force(evaluatorFor(node, db).evaluate(node));
2986
+ function evaluatedValue(node, db, bindings) {
2987
+ const evaluator = evaluatorFor(node, db);
2988
+ if (bindings === void 0) {
2989
+ return force(evaluator.evaluate(node));
2990
+ }
2991
+ const supplied = new Map(
2992
+ [...bindings].map(([name, value]) => [name, text(value)])
2993
+ );
2994
+ return force(evaluator.evaluate(node, { bindings: supplied }));
2590
2995
  }
2591
- function stringValueOf(node, db) {
2592
- return literalOf2(evaluatedValue(node, db));
2996
+ function stringValueOf(node, db, bindings) {
2997
+ return literalOf2(evaluatedValue(node, db, bindings));
2593
2998
  }
2594
2999
  function methodDefinitionsIn(file, root) {
2595
3000
  const found = /* @__PURE__ */ new Map();
@@ -3250,8 +3655,7 @@ function controllerFilters(pattern, ancestry) {
3250
3655
  skipNames.set(form.skippedBy, form.name);
3251
3656
  }
3252
3657
  }
3253
- const declared = [];
3254
- const skips = [];
3658
+ let declared = [];
3255
3659
  for (const { statement } of inheritedStatements(ancestry)) {
3256
3660
  const called = calledName(statement);
3257
3661
  if (called === null) {
@@ -3259,16 +3663,21 @@ function controllerFilters(pattern, ancestry) {
3259
3663
  }
3260
3664
  const form = byName.get(called);
3261
3665
  if (form !== void 0) {
3262
- declared.push(...declarationsOf(statement, form));
3666
+ for (const declaration of declarationsOf(statement, form)) {
3667
+ declared = [
3668
+ ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
3669
+ declaration
3670
+ ];
3671
+ }
3263
3672
  continue;
3264
3673
  }
3265
3674
  const skipped = skipNames.get(called);
3266
3675
  if (skipped !== void 0) {
3267
- skips.push(...skipsOf(statement, skipped));
3676
+ declared = applySkips(declared, skipsOf(statement, skipped));
3268
3677
  }
3269
3678
  }
3270
3679
  const resolved = [];
3271
- for (const declaration of applySkips(declared, skips)) {
3680
+ for (const declaration of declared) {
3272
3681
  const found = methodInAncestry(ancestry, declaration.methodName);
3273
3682
  if (found.type !== "found") {
3274
3683
  continue;
@@ -3381,6 +3790,9 @@ function skipsOf(statement, filterName) {
3381
3790
  const actions = namesUnder(args.keyword.only);
3382
3791
  return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
3383
3792
  }
3793
+ function sameFilter(one, other) {
3794
+ return one.filter.name === other.filter.name && one.methodName === other.methodName;
3795
+ }
3384
3796
  function applySkips(declared, skips) {
3385
3797
  const left = [];
3386
3798
  for (const declaration of declared) {
@@ -3423,13 +3835,8 @@ function namesUnder(node) {
3423
3835
  }
3424
3836
 
3425
3837
  // src/storage.ts
3426
- import { deriveOnDemand as deriveOnDemand2, evaluate as evaluate2 } from "@suss/datalog";
3427
3838
  import { storageBinding } from "@suss/ir-core";
3428
- import {
3429
- ANSWER_RELATIONS as ANSWER_RELATIONS2,
3430
- RESOLUTION_QUESTIONS as RESOLUTION_QUESTIONS2,
3431
- RESOLUTION_RULES as RESOLUTION_RULES2
3432
- } from "@suss/resolution";
3839
+ import { askResolution as askResolution2 } from "@suss/resolution";
3433
3840
  function children3(node) {
3434
3841
  return node.namedChildren.filter((child) => child !== null);
3435
3842
  }
@@ -3455,13 +3862,8 @@ function isConstant(node) {
3455
3862
  function constantName(constant2) {
3456
3863
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
3457
3864
  }
3458
- var ANCESTRY_PROGRAM = deriveOnDemand2(
3459
- [...RESOLUTION_RULES2, ...RESOLUTION_QUESTIONS2],
3460
- ANSWER_RELATIONS2
3461
- );
3462
3865
  function reachesBase(facts, classKey, bases) {
3463
- facts.add("wantedAncestry", [classKey]);
3464
- evaluate2(facts, ANCESTRY_PROGRAM.rules);
3866
+ askResolution2(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
3465
3867
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
3466
3868
  }
3467
3869
  function classBehind(facts, file, constant2) {
@@ -3508,7 +3910,7 @@ function storageEffect(call, model, pattern, kind, selector) {
3508
3910
  binding: storageBinding({
3509
3911
  recognition: "ruby-storage",
3510
3912
  storageSystem: pattern.storageSystem,
3511
- scope: pattern.baseClasses[0] ?? "",
3913
+ scope: "default",
3512
3914
  container: constantName(model)
3513
3915
  }),
3514
3916
  callee: call.text,
@@ -3676,6 +4078,7 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
3676
4078
  lookup: {
3677
4079
  root: pattern.root,
3678
4080
  pathConvention: pattern.pathConvention,
4081
+ acronyms: pattern.acronyms ?? [],
3679
4082
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3680
4083
  parsedFile: (absPath) => cache.get(absPath),
3681
4084
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
@@ -3790,12 +4193,16 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3790
4193
  const lookup = {
3791
4194
  root: pattern.root,
3792
4195
  pathConvention: pattern.pathConvention,
4196
+ acronyms: pattern.acronyms ?? [],
3793
4197
  ancestryRootClassNames: pattern.ancestryRootClassNames,
3794
4198
  parsedFile: (absPath) => options.cache.get(absPath),
3795
4199
  localDefinition: (name) => sameFileBlocks(name, fileBlocks)
3796
4200
  };
3797
4201
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, lookup);
3798
- if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
4202
+ if (!reachesConfiguredBase(ancestry, info.qualifiedName, [
4203
+ ...pattern.baseClassNames,
4204
+ ...pattern.ancestryRootClassNames
4205
+ ])) {
3799
4206
  return [];
3800
4207
  }
3801
4208
  const filters = controllerFilters(pattern, ancestry);
@@ -3815,43 +4222,73 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
3815
4222
  enclosingQualifiedName: filter.enclosingQualifiedName
3816
4223
  });
3817
4224
  }
3818
- for (const block of ownBlocks) {
3819
- if (block.info.bodyNode === null) {
4225
+ const emitAction = (actionName, method, block) => {
4226
+ const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
4227
+ (filter) => filterReference(
4228
+ filter,
4229
+ options.displayPathOf?.(filter.file) ?? filter.file
4230
+ )
4231
+ );
4232
+ const raw = buildControllerActionUnit(
4233
+ pack,
4234
+ pattern,
4235
+ info.qualifiedName,
4236
+ actionName,
4237
+ method,
4238
+ {
4239
+ display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
4240
+ absolute: block.file
4241
+ },
4242
+ options,
4243
+ around
4244
+ );
4245
+ units.push(raw);
4246
+ options.onReachSeed?.(raw, {
4247
+ file: block.file,
4248
+ node: method,
4249
+ enclosingQualifiedName: block.info.qualifiedName
4250
+ });
4251
+ };
4252
+ const own = /* @__PURE__ */ new Set();
4253
+ for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
4254
+ own.add(actionName);
4255
+ emitAction(actionName, method, block);
4256
+ }
4257
+ const seen = new Set(own);
4258
+ for (const entry of ancestry) {
4259
+ if (entry.type !== "bodies" || entry.name === info.qualifiedName) {
3820
4260
  continue;
3821
4261
  }
3822
- const visibility = instanceMethodVisibility(block.info.bodyNode);
3823
- for (const [actionName, method] of instanceMethodsByName(
3824
- block.info.bodyNode
4262
+ for (const [actionName, method, block] of publicInstanceMethods(
4263
+ entry.blocks
3825
4264
  )) {
3826
- if ((visibility.get(actionName) ?? "public") !== "public") {
4265
+ if (seen.has(actionName)) {
3827
4266
  continue;
3828
4267
  }
3829
- const around = filters.filter((filter) => filterCoversAction(filter, actionName)).map(
3830
- (filter) => filterReference(
3831
- filter,
3832
- options.displayPathOf?.(filter.file) ?? filter.file
3833
- )
3834
- );
3835
- const raw = buildControllerActionUnit(
3836
- pack,
3837
- pattern,
3838
- info.qualifiedName,
3839
- actionName,
3840
- method,
3841
- { display: options.filePath, absolute: block.file },
3842
- options,
3843
- around
3844
- );
3845
- units.push(raw);
3846
- options.onReachSeed?.(raw, {
3847
- file: block.file,
3848
- node: method,
3849
- enclosingQualifiedName: info.qualifiedName
3850
- });
4268
+ seen.add(actionName);
4269
+ if (pattern.routeFor(info.qualifiedName, actionName) === null) {
4270
+ continue;
4271
+ }
4272
+ emitAction(actionName, method, block);
3851
4273
  }
3852
4274
  }
3853
4275
  return units;
3854
4276
  }
4277
+ function publicInstanceMethods(blocks) {
4278
+ const found = [];
4279
+ for (const block of blocks) {
4280
+ if (block.info.bodyNode === null) {
4281
+ continue;
4282
+ }
4283
+ const visibility = instanceMethodVisibility(block.info.bodyNode);
4284
+ for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
4285
+ if ((visibility.get(name) ?? "public") === "public") {
4286
+ found.push([name, method, block]);
4287
+ }
4288
+ }
4289
+ }
4290
+ return found;
4291
+ }
3855
4292
  function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actionName, method, file, bodyRead, wrappers = []) {
3856
4293
  const range = rangeOf(method);
3857
4294
  const route = pattern.routeFor(controllerQualifiedName, actionName);
@@ -4364,7 +4801,8 @@ function collectFileConstants(filePath, root) {
4364
4801
  if (written !== null) {
4365
4802
  definitions.push({
4366
4803
  qualifiedName: qualify(nesting, written),
4367
- key: nodeId(filePath, node)
4804
+ key: nodeId(filePath, node),
4805
+ kind: "declaration"
4368
4806
  });
4369
4807
  const inside = [...nesting, ...written.split("::")];
4370
4808
  for (const child of children4(node)) {
@@ -4379,7 +4817,8 @@ function collectFileConstants(filePath, root) {
4379
4817
  if (left !== null && left.type === "constant" && right !== null) {
4380
4818
  definitions.push({
4381
4819
  qualifiedName: qualify(nesting, left.text),
4382
- key: nodeId(filePath, right)
4820
+ key: nodeId(filePath, right),
4821
+ kind: "assignment"
4383
4822
  });
4384
4823
  }
4385
4824
  }
@@ -4406,14 +4845,18 @@ function emitConstantBindings(db, perFile) {
4406
4845
  for (const file of files) {
4407
4846
  for (const definition of file.definitions) {
4408
4847
  const found = byName.get(definition.qualifiedName) ?? [];
4409
- found.push(definition.key);
4848
+ found.push(definition);
4410
4849
  byName.set(definition.qualifiedName, found);
4411
4850
  fileOfDefinition.set(definition.key, file.filePath);
4412
4851
  }
4413
4852
  }
4414
4853
  const settles = (candidate) => {
4415
- const found = byName.get(candidate);
4416
- 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;
4417
4860
  };
4418
4861
  for (const file of files) {
4419
4862
  for (const reference of file.references) {
@@ -4440,7 +4883,7 @@ function emitConstantBindings(db, perFile) {
4440
4883
  import fs4 from "fs";
4441
4884
  import path5 from "path";
4442
4885
  import { Database as Database3 } from "@suss/datalog";
4443
- import { explainResolvedKey, RESOLUTION_RULES as RESOLUTION_RULES3 } from "@suss/resolution";
4886
+ import { explainResolvedKey, RESOLUTION_RULES } from "@suss/resolution";
4444
4887
 
4445
4888
  // src/parser.ts
4446
4889
  import fs2 from "fs";
@@ -4588,34 +5031,118 @@ import {
4588
5031
  unfollowedCallGap,
4589
5032
  worthRecording
4590
5033
  } from "@suss/behavioral-ir";
4591
- 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";
4592
5035
  import { assembleSummary } from "@suss/extractor";
4593
5036
 
4594
5037
  // src/reach/resolveCallee.ts
5038
+ import { calleeOutcomeOf, calleeOutcomes } from "@suss/resolution";
4595
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
+ };
4596
5047
  var stop = (reason) => ({
4597
5048
  kind: "stopped",
4598
5049
  reason
4599
5050
  });
5051
+ var NO_DECLARATION = stop("noDeclaration");
4600
5052
  function followed(target) {
4601
5053
  return { kind: "followed", target };
4602
5054
  }
4603
- 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) {
4604
5082
  const methodName = calleeMethodName(call);
4605
5083
  if (methodName === void 0) {
4606
- return stop("noDeclaration");
5084
+ return { kind: "stopped", reason: "noDeclaration" };
4607
5085
  }
4608
5086
  if (DYNAMIC_SEND_NAMES.has(methodName)) {
4609
- return stop("unsettledValue");
5087
+ return { kind: "stopped", reason: "unsettledValue" };
4610
5088
  }
4611
5089
  const receiver = field(call, "receiver");
4612
5090
  if (receiver === null || receiver.type === "self") {
4613
- 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);
5114
+ }
5115
+ if (outcome.kind === "function") {
5116
+ return spelling.method === INVOKES_RECEIVER ? functionCallee(outcome.key, ctx) : NO_DECLARATION;
5117
+ }
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;
4614
5131
  }
4615
- if (receiver.type === "identifier" && methodName === "call" && site.ownParameters.includes(receiver.text)) {
4616
- return stop("callerSupplied");
5132
+ if (!spelling.onClassItself) {
5133
+ return methodOnAncestryOf(qualifiedName, spelling.method, ctx);
4617
5134
  }
4618
- return resolveOnReceiver(receiver, methodName, site, ctx);
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);
4619
5146
  }
4620
5147
  async function resolveMethodReference(name, site, ctx) {
4621
5148
  const resolved = await resolveImplicitSelf(name, site, ctx);
@@ -4640,58 +5167,13 @@ async function resolveImplicitSelf(methodName, site, ctx) {
4640
5167
  function resolveTopLevelName(methodName, ctx) {
4641
5168
  const candidates = ctx.topLevelMethods.get(methodName);
4642
5169
  if (candidates === void 0 || candidates.length === 0) {
4643
- return stop("noDeclaration");
5170
+ return NO_DECLARATION;
4644
5171
  }
4645
5172
  if (candidates.length > 1) {
4646
5173
  return stop("multipleSources");
4647
5174
  }
4648
5175
  return followed(candidates[0]);
4649
5176
  }
4650
- async function resolveOnReceiver(receiver, methodName, site, ctx) {
4651
- if (receiver.type === "call") {
4652
- const innerName = calleeMethodName(receiver);
4653
- const innerReceiver = field(receiver, "receiver");
4654
- if (innerName === "new" && innerReceiver !== null) {
4655
- const classRef = classRefOf(innerReceiver, site, ctx);
4656
- return classRef === null ? stop("unsettledValue") : methodOnAncestryOf(classRef, methodName, ctx);
4657
- }
4658
- const root = rootConstantOf(receiver);
4659
- if (root === null) {
4660
- return stop("unsettledValue");
4661
- }
4662
- return classRefOf(root, site, ctx) === null ? stop("outsideRun") : stop("unsettledValue");
4663
- }
4664
- if (receiver.type === "constant" || receiver.type === "scope_resolution") {
4665
- const classRef = classRefOf(receiver, site, ctx);
4666
- return classRef === null ? stop("outsideRun") : singletonMethodOn(classRef, methodName, ctx);
4667
- }
4668
- return stop("unsettledValue");
4669
- }
4670
- function rootConstantOf(node) {
4671
- if (node.type === "constant" || node.type === "scope_resolution") {
4672
- return node;
4673
- }
4674
- if (node.type !== "call") {
4675
- return null;
4676
- }
4677
- const receiver = field(node, "receiver");
4678
- return receiver === null ? null : rootConstantOf(receiver);
4679
- }
4680
- function classRefOf(node, site, ctx) {
4681
- if (node.type === "constant") {
4682
- const shadow = shadowingClassFor(node, site.nesting, ctx.knownClasses);
4683
- if (shadow !== null) {
4684
- return shadow;
4685
- }
4686
- const qualified = qualifyConstantRef(node, site.nesting);
4687
- return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
4688
- }
4689
- if (node.type === "scope_resolution") {
4690
- const qualified = qualifyConstantRef(node, site.nesting);
4691
- return qualified !== null && ctx.knownClasses.has(qualified) ? qualified : null;
4692
- }
4693
- return null;
4694
- }
4695
5177
  async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
4696
5178
  const ownBlocks = ctx.lookup.localDefinition?.(qualifiedName) ?? null;
4697
5179
  if (ownBlocks === null) {
@@ -4707,7 +5189,7 @@ async function methodOnAncestryOf(qualifiedName, methodName, ctx) {
4707
5189
  found.cause === "dynamicDefine" ? "unsettledValue" : "outsideRun"
4708
5190
  );
4709
5191
  }
4710
- return stop("noDeclaration");
5192
+ return NO_DECLARATION;
4711
5193
  }
4712
5194
  function reachedMethod(node, block, name) {
4713
5195
  return {
@@ -4732,7 +5214,7 @@ function singletonMethodOn(qualifiedName, methodName, ctx) {
4732
5214
  return followed(reachedMethod(found, block, methodName));
4733
5215
  }
4734
5216
  }
4735
- return stop("noDeclaration");
5217
+ return NO_DECLARATION;
4736
5218
  }
4737
5219
 
4738
5220
  // src/reach/closure.ts
@@ -4745,7 +5227,7 @@ var REACHABLE_RULES = [
4745
5227
  )
4746
5228
  ];
4747
5229
  async function reachedFunctions(seeds, options) {
4748
- const ctx = buildReachContext(options.files);
5230
+ const ctx = buildReachContext(options.files, options.facts);
4749
5231
  const db = new Database();
4750
5232
  const functionByKey = /* @__PURE__ */ new Map();
4751
5233
  const seedKeys = /* @__PURE__ */ new Set();
@@ -4767,18 +5249,26 @@ async function reachedFunctions(seeds, options) {
4767
5249
  db.add("entry", [seed.key]);
4768
5250
  }
4769
5251
  for (; ; ) {
4770
- evaluate3(db, REACHABLE_RULES);
5252
+ evaluate(db, REACHABLE_RULES);
4771
5253
  const frontier = db.facts("reachable").map(([key]) => String(key)).filter((key) => !scanned.has(key));
4772
5254
  if (frontier.length === 0) {
4773
5255
  break;
4774
5256
  }
4775
- for (const key of frontier) {
5257
+ const bodies = frontier.flatMap((key) => {
4776
5258
  scanned.add(key);
4777
5259
  const source = functionByKey.get(key);
4778
- if (source === void 0) {
4779
- continue;
4780
- }
4781
- 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
+ });
4782
5272
  if (scan.stops.length > 0) {
4783
5273
  stopsByKey.set(key, scan.stops);
4784
5274
  }
@@ -4831,11 +5321,22 @@ async function reachedFunctions(seeds, options) {
4831
5321
  function keyOf(target) {
4832
5322
  return nodeId(target.file, target.node);
4833
5323
  }
4834
- function buildReachContext(files) {
5324
+ var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5325
+ function buildReachContext(files, facts) {
4835
5326
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
4836
5327
  const classes = [];
5328
+ const classNames = /* @__PURE__ */ new Map();
5329
+ const classOfMethod = /* @__PURE__ */ new Map();
4837
5330
  for (const { file, root } of files) {
4838
- 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
+ });
4839
5340
  }
4840
5341
  const knownClasses = new Set(classes.map(({ info }) => info.qualifiedName));
4841
5342
  for (const { file, info } of classes) {
@@ -4843,22 +5344,33 @@ function buildReachContext(files) {
4843
5344
  list.push({ info, knownClasses, file });
4844
5345
  blocksByQualifiedName.set(info.qualifiedName, list);
4845
5346
  }
4846
- const topLevelMethods = /* @__PURE__ */ new Map();
5347
+ const definitions = /* @__PURE__ */ new Map();
4847
5348
  for (const { file, root } of files) {
4848
- for (const method of topLevelMethodNodes(root)) {
5349
+ for (const [key, method] of methodDefinitionsIn(file, root)) {
4849
5350
  const name = field(method, "name")?.text;
4850
5351
  if (name === void 0) {
4851
5352
  continue;
4852
5353
  }
4853
- const list = topLevelMethods.get(name) ?? [];
4854
- list.push({
5354
+ const owner = classOfMethod.get(key) ?? null;
5355
+ definitions.set(key, {
4855
5356
  file,
4856
5357
  node: method,
4857
5358
  name,
4858
- exportPath: [name],
4859
- enclosingQualifiedName: null
5359
+ exportPath: owner === null ? [name] : [owner, name],
5360
+ enclosingQualifiedName: owner
4860
5361
  });
4861
- 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);
4862
5374
  }
4863
5375
  }
4864
5376
  const lookup = {
@@ -4871,7 +5383,13 @@ function buildReachContext(files) {
4871
5383
  parsedFile: async () => null,
4872
5384
  localDefinition: (name) => blocksByQualifiedName.get(name) ?? null
4873
5385
  };
4874
- 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);
4875
5393
  }
4876
5394
  function topLevelMethodNodes(root, found = []) {
4877
5395
  for (const child of bodyStatements(root)) {
@@ -4886,14 +5404,6 @@ function topLevelMethodNodes(root, found = []) {
4886
5404
  }
4887
5405
  return found;
4888
5406
  }
4889
- function nestingOf(qualifiedName) {
4890
- const parts = qualifiedName.split("::");
4891
- const out = [];
4892
- for (let depth = parts.length; depth >= 1; depth--) {
4893
- out.push(parts.slice(0, depth).join("::"));
4894
- }
4895
- return out;
4896
- }
4897
5407
  var EMPTY_SCAN = {
4898
5408
  followed: [],
4899
5409
  stops: [],
@@ -4911,21 +5421,23 @@ function methodReferenceSymbol(node) {
4911
5421
  const argChildren = args === null ? [] : bodyStatements(args);
4912
5422
  return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
4913
5423
  }
4914
- 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) {
4915
5435
  const displayPathOf = options.displayPathOf;
4916
- const body = field(source.node, "body");
4917
- if (body === null) {
5436
+ if (field(source.node, "body") === null) {
4918
5437
  return EMPTY_SCAN;
4919
5438
  }
4920
- const calls = withoutChainLinks(
4921
- bodyCalls(source.node, options.inheritedMethods)
4922
- );
5439
+ const { calls, site } = read;
4923
5440
  const ownParameters = positionalParameters(source.node).map((p) => p.name);
4924
- const site = {
4925
- enclosingQualifiedName: source.enclosingQualifiedName,
4926
- nesting: source.enclosingQualifiedName === null ? [] : nestingOf(source.enclosingQualifiedName),
4927
- ownParameters
4928
- };
4929
5441
  const followed2 = [];
4930
5442
  const stops = [];
4931
5443
  const placements = new TargetPlacements();
@@ -4968,7 +5480,7 @@ async function scanBody(source, ctx, options) {
4968
5480
  };
4969
5481
  for (const call of calls) {
4970
5482
  const callee = calleeText(call);
4971
- const outcome = await resolveCallee(call, site, ctx);
5483
+ const outcome = await resolveCallee(call, site, ctx, read.spellings);
4972
5484
  const placed = outcome.kind === "followed" ? {
4973
5485
  file: displayPathOf(outcome.target.file),
4974
5486
  span: spanOf(outcome.target.node)
@@ -5106,7 +5618,8 @@ function inheritedMethodsIn(packs) {
5106
5618
  return found;
5107
5619
  }
5108
5620
  function alreadyDiscovered(seen, raw) {
5109
- const key = `${raw.identity.file}::${raw.identity.name}::${raw.identity.range.start}`;
5621
+ const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
5622
+ const key = `${raw.identity.file}::${reported}::${raw.identity.range.start}`;
5110
5623
  if (seen.has(key)) {
5111
5624
  return true;
5112
5625
  }
@@ -5266,6 +5779,7 @@ async function extractRubyProject(options) {
5266
5779
  () => reachedFunctions(seeds, {
5267
5780
  files: parsed,
5268
5781
  displayPathOf,
5782
+ facts: db,
5269
5783
  ...storage === void 0 ? {} : { storage },
5270
5784
  inheritedMethods
5271
5785
  })
@@ -5362,15 +5876,15 @@ function findRubyFiles(root) {
5362
5876
  }
5363
5877
 
5364
5878
  // src/facts/why.ts
5365
- var WITNESS_RULES = [...RESOLUTION_RULES3, ...RUBY_RULES];
5366
- 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"]);
5367
5881
  function namedChildrenOf(node) {
5368
5882
  return node.namedChildren.filter((child) => child !== null);
5369
5883
  }
5370
5884
  function enclosingMethod2(node) {
5371
5885
  let current = node.parent;
5372
5886
  while (current !== null) {
5373
- if (METHOD_TYPES4.has(current.type)) {
5887
+ if (METHOD_TYPES5.has(current.type)) {
5374
5888
  return current;
5375
5889
  }
5376
5890
  current = current.parent;
@@ -5405,8 +5919,8 @@ function displayNameOf(node) {
5405
5919
  if (node.type === "class" || node.type === "module") {
5406
5920
  return field(node, "name")?.text ?? node.text;
5407
5921
  }
5408
- const text = node.text.split("\n")[0].trim();
5409
- return text.length > 40 ? `${text.slice(0, 40)}...` : text;
5922
+ const text2 = node.text.split("\n")[0].trim();
5923
+ return text2.length > 40 ? `${text2.slice(0, 40)}...` : text2;
5410
5924
  }
5411
5925
  function width(node) {
5412
5926
  return node.endIndex - node.startIndex;
@@ -5433,14 +5947,14 @@ var RubyWhySession = class {
5433
5947
  * The smallest expression on `line` of `file` whose text is exactly
5434
5948
  * `text`, or null.
5435
5949
  */
5436
- findExpression(file, line, text) {
5950
+ findExpression(file, line, text2) {
5437
5951
  const root = this.rootOf(file);
5438
5952
  if (root === null) {
5439
5953
  return null;
5440
5954
  }
5441
5955
  let found = null;
5442
5956
  const visit = (node) => {
5443
- if (node.startPosition.row + 1 === line && node.text === text) {
5957
+ if (node.startPosition.row + 1 === line && node.text === text2) {
5444
5958
  if (found === null || width(node) <= width(found)) {
5445
5959
  found = node;
5446
5960
  }
@@ -5610,6 +6124,7 @@ export {
5610
6124
  inheritedStatements,
5611
6125
  methodDefinitionsIn,
5612
6126
  methodInAncestry,
6127
+ nestedStatements,
5613
6128
  nodeId,
5614
6129
  parseRuby,
5615
6130
  parseRubySync,