@suss/adapter-ruby 0.29.0 → 0.30.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
@@ -143,7 +143,52 @@ var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
143
143
  "singleton_class"
144
144
  ]);
145
145
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
146
+ var INCLUDE_CALL = "include";
147
+ var PREPEND_CALL = "prepend";
146
148
  var ARGUMENT_LIST_TYPE = "argument_list";
149
+ var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
150
+ var NO_BODY_BLOCKS = /* @__PURE__ */ new Map();
151
+ function isModuleBody(body) {
152
+ return body.parent?.type === "module";
153
+ }
154
+ function declaredBlockAt(statement, isModule, blocks) {
155
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
156
+ return null;
157
+ }
158
+ const method = field(statement, "method");
159
+ const kind = method === null ? void 0 : blocks.get(method.text);
160
+ if (kind === void 0 || kind.moduleOnly && !isModule) {
161
+ return null;
162
+ }
163
+ return field(statement, "block") === null ? null : kind;
164
+ }
165
+ function bodyStatementsRun(body, isModule, blocks) {
166
+ return bodyStatements(body).flatMap((statement) => {
167
+ if (declaredBlockAt(statement, isModule, blocks) === null) {
168
+ return [statement];
169
+ }
170
+ const block = field(statement, "block");
171
+ const inner = block === null ? null : field(block, "body");
172
+ return inner === null ? [] : bodyStatementsRun(inner, isModule, blocks);
173
+ });
174
+ }
175
+ function definedAtClassLevel(node, body, blocks) {
176
+ if (blocks.size === 0) {
177
+ return false;
178
+ }
179
+ const isModule = isModuleBody(body);
180
+ let current = node.parent;
181
+ while (current !== null && current.id !== body.id) {
182
+ const parent = current.parent;
183
+ if (BLOCK_TYPES.has(current.type) && parent !== null) {
184
+ if (declaredBlockAt(parent, isModule, blocks)?.definesClassMethods) {
185
+ return true;
186
+ }
187
+ }
188
+ current = parent;
189
+ }
190
+ return false;
191
+ }
147
192
  function runStatements(body, blockConfigures = () => false) {
148
193
  const found = [];
149
194
  const readsItsChildren = (node) => {
@@ -179,10 +224,10 @@ function stringLiteralValue(node) {
179
224
  function symbolValue(node) {
180
225
  return node.type === "simple_symbol" ? node.text.slice(1) : null;
181
226
  }
182
- function instanceMethodsByName(body) {
227
+ function instanceMethodsByName(body, blocks = NO_BODY_BLOCKS) {
183
228
  const methods = /* @__PURE__ */ new Map();
184
229
  for (const stmt of runStatements(body)) {
185
- if (stmt.type !== "method") {
230
+ if (stmt.type !== "method" || definedAtClassLevel(stmt, body, blocks)) {
186
231
  continue;
187
232
  }
188
233
  const name = field(stmt, "name")?.text;
@@ -244,10 +289,11 @@ function instanceMethodVisibility(body) {
244
289
  }
245
290
  return visibility;
246
291
  }
247
- function singletonMethodsByName(body) {
292
+ function singletonMethodsByName(body, blocks = NO_BODY_BLOCKS) {
248
293
  const methods = /* @__PURE__ */ new Map();
249
294
  for (const stmt of runStatements(body)) {
250
- if (stmt.type !== "singleton_method") {
295
+ const classLevel = stmt.type === "method" && definedAtClassLevel(stmt, body, blocks);
296
+ if (stmt.type !== "singleton_method" && !classLevel) {
251
297
  continue;
252
298
  }
253
299
  const name = field(stmt, "name")?.text;
@@ -360,6 +406,7 @@ function autoloadDirectories(root) {
360
406
  }
361
407
 
362
408
  // src/defineMethod.ts
409
+ var import_resolution5 = require("@suss/resolution");
363
410
  var import_values4 = require("@suss/values");
364
411
 
365
412
  // src/values/evaluator.ts
@@ -396,6 +443,29 @@ var RUBY_RULES = [
396
443
  (0, import_datalog.lit)("givesBackOne", (0, import_datalog.variable)("n"), (0, import_datalog.variable)("m"))
397
444
  ],
398
445
  "declared finder"
446
+ ),
447
+ // Ruby cannot refer to a method without running it, so
448
+ // `Settings.filters` is worth what the method returns. The shared
449
+ // rules hang that step on a `call` fact this spelling never gets.
450
+ (0, import_datalog.rule)(
451
+ "invokes",
452
+ [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("f")],
453
+ [
454
+ (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), (0, import_datalog.variable)("m")),
455
+ (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("f")),
456
+ (0, import_datalog.lit)("func", (0, import_datalog.variable)("f"))
457
+ ],
458
+ "property read runs a method"
459
+ ),
460
+ // `%i[a b].freeze` is worth the list it was written as. The evaluator
461
+ // says the same in its row table, for a value it reads in one file.
462
+ ...["freeze", "dup"].map(
463
+ (method) => (0, import_datalog.rule)(
464
+ "stepsTo",
465
+ [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), import_resolution.VALUE_STEP],
466
+ [(0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)(method))],
467
+ "hands back the receiver"
468
+ )
399
469
  )
400
470
  ];
401
471
  var RUBY_PROGRAM = (0, import_resolution.resolutionProgram)(RUBY_RULES);
@@ -415,8 +485,8 @@ var import_resolution3 = require("@suss/resolution");
415
485
 
416
486
  // src/facts/locals.ts
417
487
  var import_resolution2 = require("@suss/resolution");
418
- var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
419
- var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES]);
488
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
489
+ var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES2]);
420
490
  var RUBY_NAME_TYPES = /* @__PURE__ */ new Set([
421
491
  "identifier",
422
492
  "instance_variable"
@@ -569,7 +639,7 @@ function ownerOfName(node, name, enclosing) {
569
639
  function blockDeclaring(node, name) {
570
640
  let current = node.parent;
571
641
  while (current !== null) {
572
- if (BLOCK_TYPES.has(current.type) && parameterNames(current).has(name)) {
642
+ if (BLOCK_TYPES2.has(current.type) && parameterNames(current).has(name)) {
573
643
  return current;
574
644
  }
575
645
  if (OWN_BODY_TYPES.has(current.type)) {
@@ -819,17 +889,70 @@ function emitHash(emitter, hash) {
819
889
  add(emitter, "holdsProperty", objectKey, keyText, valueKey(emitter, value));
820
890
  }
821
891
  }
822
- function walkExpressions(node, visit) {
892
+ var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
893
+ var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
894
+ var DEFINE_METHOD_CALL = "define_method";
895
+ function loopTurnAt(emitter, block) {
896
+ const call = block.parent;
897
+ if (call?.type !== "call") {
898
+ return null;
899
+ }
900
+ const receiver = field(call, "receiver");
901
+ const method = field(call, "method");
902
+ if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
903
+ return null;
904
+ }
905
+ const parameters = field(block, "parameters");
906
+ const names = parameters === null ? [] : children(parameters).map((p) => p.text);
907
+ const element = names[0];
908
+ if (element === void 0) {
909
+ return null;
910
+ }
911
+ return {
912
+ element,
913
+ index: names[1] ?? "",
914
+ overKey: nodeId(emitter.filePath, receiver)
915
+ };
916
+ }
917
+ function emitDynamicDefinition(emitter, call, turns) {
918
+ if (emitter.selfKey === null || // A call inside a method runs when that method does, not at load time.
919
+ emitter.enclosing !== null || call.type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
920
+ return;
921
+ }
922
+ const args = field(call, "arguments");
923
+ const first = args === null ? void 0 : children(args)[0];
924
+ const nameKey2 = nodeId(emitter.filePath, first ?? call);
925
+ add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
926
+ for (const turn of turns) {
927
+ add(
928
+ emitter,
929
+ "nameTurnsOn",
930
+ nameKey2,
931
+ turn.element,
932
+ turn.index,
933
+ turn.overKey
934
+ );
935
+ }
936
+ }
937
+ function walkExpressions(node, emitter, visit, turns = []) {
823
938
  for (const child of children(node)) {
824
939
  if (OWN_BODY_TYPES.has(child.type)) {
825
940
  continue;
826
941
  }
827
- visit(child);
828
- walkExpressions(child, visit);
942
+ visit(child, turns);
943
+ const opened = BLOCK_TYPES3.has(child.type) ? loopTurnAt(emitter, child) : null;
944
+ walkExpressions(
945
+ child,
946
+ emitter,
947
+ visit,
948
+ opened === null ? turns : [...turns, opened]
949
+ );
829
950
  }
830
951
  }
831
952
  function emitExpressionFacts(emitter, node) {
832
- walkExpressions(node, (child) => {
953
+ emitDynamicDefinition(emitter, node, []);
954
+ walkExpressions(node, emitter, (child, turns) => {
955
+ emitDynamicDefinition(emitter, child, turns);
833
956
  if (isPropertyRead(child)) {
834
957
  emitPropertyRead(emitter, child);
835
958
  } else if (child.type === "call") {
@@ -918,7 +1041,20 @@ function implicitReturn(body) {
918
1041
  (child) => child.type !== "rescue" && child.type !== "ensure"
919
1042
  );
920
1043
  const last = statements[statements.length - 1];
921
- return last === void 0 || last.type === "return" ? null : last;
1044
+ if (last === void 0 || last.type === "return") {
1045
+ return null;
1046
+ }
1047
+ return assignedValueOf(last) ?? last;
1048
+ }
1049
+ function assignedValueOf(node) {
1050
+ if (!ASSIGNMENT_TYPES.has(node.type)) {
1051
+ return null;
1052
+ }
1053
+ const operator = field(node, "operator")?.text;
1054
+ if (node.type !== "assignment" && !WHOLE_VALUE_OPERATORS.has(operator ?? "")) {
1055
+ return null;
1056
+ }
1057
+ return field(node, "right");
922
1058
  }
923
1059
  function emitMethodFacts(emitter, method) {
924
1060
  const funcKey = nodeId(emitter.filePath, method);
@@ -948,7 +1084,7 @@ function emitMethodFacts(emitter, method) {
948
1084
  }
949
1085
  };
950
1086
  recordNested(body);
951
- walkExpressions(body, (child) => {
1087
+ walkExpressions(body, inside, (child) => {
952
1088
  if (child.type === "return") {
953
1089
  const first = children(child)[0];
954
1090
  const returned = first?.type === "argument_list" ? children(first)[0] : first;
@@ -1038,23 +1174,37 @@ function emitCandidates(emitter, key, group) {
1038
1174
  }
1039
1175
  }
1040
1176
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1177
+ var CONSTANT_REF_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
1178
+ function mixedInConstants(body, callName) {
1179
+ return bareCallArgumentGroups(body, callName).flatMap((group) => [...group].reverse()).reverse().map(readThrough).filter((argument) => CONSTANT_REF_TYPES.has(argument.type));
1180
+ }
1181
+ function emitMixinFacts(emitter, classKey, body, callName) {
1182
+ for (const mixin of mixedInConstants(body, callName)) {
1183
+ add(emitter, "extends", classKey, valueKey(emitter, mixin));
1184
+ }
1185
+ }
1041
1186
  function emitClassFacts(emitter, cls) {
1042
1187
  const classKey = nodeId(emitter.filePath, cls);
1043
1188
  add(emitter, "objectValue", classKey);
1189
+ const body = field(cls, "body");
1190
+ if (body !== null) {
1191
+ emitMixinFacts(emitter, classKey, body, PREPEND_CALL);
1192
+ emitMixinFacts(emitter, classKey, body, INCLUDE_CALL);
1193
+ }
1044
1194
  const superclass = field(cls, "superclass");
1045
1195
  const base = superclass === null ? null : children(superclass)[0] ?? null;
1046
1196
  if (base !== null) {
1047
1197
  add(emitter, "extends", classKey, valueKey(emitter, base));
1048
1198
  add(emitter, "extendsNamed", classKey, base.text);
1049
1199
  }
1050
- const body = field(cls, "body");
1051
1200
  const collected = /* @__PURE__ */ new Map();
1052
1201
  const within = {
1053
1202
  ...emitter,
1054
1203
  selfKey: classKey,
1055
1204
  instanceWrites: collected
1056
1205
  };
1057
- for (const statement of body === null ? [] : children(body)) {
1206
+ const statements = body === null ? [] : bodyStatementsRun(body, cls.type === "module", emitter.bodyBlocks);
1207
+ for (const statement of statements) {
1058
1208
  if (statement.type === "assignment") {
1059
1209
  const left = field(statement, "left");
1060
1210
  const right = field(statement, "right");
@@ -1075,6 +1225,7 @@ function emitClassFacts(emitter, cls) {
1075
1225
  continue;
1076
1226
  }
1077
1227
  if (!METHOD_TYPES.has(statement.type)) {
1228
+ emitExpressionFacts(within, statement);
1078
1229
  continue;
1079
1230
  }
1080
1231
  const funcKey = emitMethodFacts(within, statement);
@@ -1086,13 +1237,14 @@ function emitClassFacts(emitter, cls) {
1086
1237
  emitInstanceWrites(within, classKey, collected);
1087
1238
  return classKey;
1088
1239
  }
1089
- function emitValueFacts(db, filePath, root) {
1240
+ function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
1090
1241
  const emitter = {
1091
1242
  db,
1092
1243
  filePath,
1093
1244
  enclosing: null,
1094
1245
  selfKey: null,
1095
- instanceWrites: null
1246
+ instanceWrites: null,
1247
+ bodyBlocks
1096
1248
  };
1097
1249
  const declaresName = (child, key) => {
1098
1250
  const name = field(child, "name");
@@ -1132,7 +1284,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1132
1284
  "module",
1133
1285
  "singleton_class"
1134
1286
  ]);
1135
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1287
+ var BLOCK_TYPES4 = /* @__PURE__ */ new Set(["block", "do_block"]);
1136
1288
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1137
1289
  var NAME_TYPES = /* @__PURE__ */ new Set([
1138
1290
  "identifier",
@@ -1641,7 +1793,7 @@ function isTailExpression(node) {
1641
1793
  if (owner.type === "block" && owner.parent?.type === "lambda") {
1642
1794
  return true;
1643
1795
  }
1644
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
1796
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES4.has(owner.type)) {
1645
1797
  return true;
1646
1798
  }
1647
1799
  if (owner.type === "when") {
@@ -1660,7 +1812,7 @@ function isRoot(node) {
1660
1812
  if (OWN_ROOT_TYPES.has(node.type)) {
1661
1813
  return true;
1662
1814
  }
1663
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
1815
+ return BLOCK_TYPES4.has(node.type) && node.parent?.type === "call";
1664
1816
  }
1665
1817
  function isPathStatement(node, parent) {
1666
1818
  if (node.type === "elsif") {
@@ -1714,7 +1866,7 @@ function functionOf(node) {
1714
1866
  body: bodyOf(block)
1715
1867
  };
1716
1868
  }
1717
- if (BLOCK_TYPES2.has(node.type)) {
1869
+ if (BLOCK_TYPES4.has(node.type)) {
1718
1870
  return {
1719
1871
  parameters: parametersOf2(field(node, "parameters")),
1720
1872
  body: bodyOf(node)
@@ -2185,113 +2337,119 @@ function contextOver(db, nodes) {
2185
2337
  }
2186
2338
 
2187
2339
  // src/defineMethod.ts
2188
- var DEFINE_METHOD_CALL = "define_method";
2189
- var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
2190
- var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
2191
- var NOTHING = { names: /* @__PURE__ */ new Set(), unreadable: false };
2192
- var byTree = /* @__PURE__ */ new WeakMap();
2193
- function defineMethodNames(body) {
2194
- let perTree = byTree.get(body.tree);
2195
- if (perTree === void 0) {
2196
- perTree = /* @__PURE__ */ new Map();
2197
- byTree.set(body.tree, perTree);
2198
- }
2199
- const cached = perTree.get(body.id);
2200
- if (cached !== void 0) {
2201
- return cached;
2202
- }
2203
- const read = readNames(body);
2204
- perTree.set(body.id, read);
2205
- return read;
2206
- }
2207
- function readNames(body) {
2208
- const calls = bareCalls(body, DEFINE_METHOD_CALL);
2209
- if (calls.length === 0) {
2210
- return NOTHING;
2340
+ var NOTHING = {
2341
+ names: /* @__PURE__ */ new Set(),
2342
+ patterns: [],
2343
+ unreadable: false
2344
+ };
2345
+ function couldBeDefined(defined, name) {
2346
+ return defined.unreadable || defined.patterns.some((pattern) => pattern.test(name));
2347
+ }
2348
+ function definedNamesOf(dynamic, classKey) {
2349
+ return dynamic?.get(classKey) ?? NOTHING;
2350
+ }
2351
+ function readDynamicNames(db, rootsByFile) {
2352
+ const byClass = /* @__PURE__ */ new Map();
2353
+ for (const [classKey, nameKey2] of db.facts("definesMethodFrom")) {
2354
+ const key = String(classKey);
2355
+ byClass.set(
2356
+ key,
2357
+ foldReading(
2358
+ byClass.get(key) ?? NOTHING,
2359
+ readOne(db, rootsByFile, String(nameKey2))
2360
+ )
2361
+ );
2211
2362
  }
2212
- const names = /* @__PURE__ */ new Set();
2213
- let unreadable = false;
2214
- for (const call of calls) {
2215
- const defined = namesDefinedBy(call);
2216
- if (defined === null) {
2217
- unreadable = true;
2218
- continue;
2219
- }
2220
- for (const name of defined) {
2221
- names.add(name);
2363
+ for (const [classKey, reading] of byClass) {
2364
+ for (const name of reading.names) {
2365
+ db.add("declaresName", [classKey, name]);
2222
2366
  }
2223
2367
  }
2224
- return { names, unreadable };
2368
+ return byClass;
2225
2369
  }
2226
- function namesDefinedBy(call) {
2227
- const args = field(call, "arguments");
2228
- const first = args === null ? void 0 : bodyStatements(args)[0];
2229
- if (first === void 0) {
2230
- return null;
2370
+ function foldReading(soFar, reading) {
2371
+ if (reading === null) {
2372
+ return { ...soFar, unreadable: true };
2231
2373
  }
2374
+ return {
2375
+ names: /* @__PURE__ */ new Set([...soFar.names, ...reading.names]),
2376
+ patterns: [...soFar.patterns, ...reading.patterns],
2377
+ unreadable: soFar.unreadable
2378
+ };
2379
+ }
2380
+ function readOne(db, rootsByFile, nameKey2) {
2381
+ const node = (0, import_resolution5.nodeOfKey)(rootsByFile, nameKey2);
2232
2382
  const names = [];
2233
- for (const bindings of loopTurns(call)) {
2234
- const name = stringValueOf(
2235
- first,
2236
- void 0,
2237
- bindings.size === 0 ? void 0 : bindings
2238
- );
2239
- if (name === null) {
2383
+ const patterns = [];
2384
+ for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
2385
+ const value = node === null ? (0, import_values4.hole)(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
2386
+ const name = (0, import_values4.literalOf)(value);
2387
+ if (name !== null) {
2388
+ names.push(name);
2389
+ continue;
2390
+ }
2391
+ const pattern = patternOf(value);
2392
+ if (pattern === null) {
2240
2393
  return null;
2241
2394
  }
2242
- names.push(name);
2395
+ patterns.push(pattern);
2243
2396
  }
2244
- return names;
2397
+ return { names, patterns };
2245
2398
  }
2246
- function loopTurns(call) {
2247
- let turns = [/* @__PURE__ */ new Map()];
2248
- for (const taken of blocksAround(call)) {
2249
- const perElement = elementBindings(taken);
2250
- if (perElement === null) {
2399
+ function patternOf(value) {
2400
+ const pieces = (0, import_values4.piecesOf)(value);
2401
+ let source = "^";
2402
+ let readSomething = false;
2403
+ for (const piece of pieces) {
2404
+ if (piece.kind === "hole") {
2405
+ source += "[\\s\\S]*";
2406
+ continue;
2407
+ }
2408
+ const options = piece.options.filter((option) => option.length > 0);
2409
+ if (options.length === 0) {
2251
2410
  continue;
2252
2411
  }
2412
+ readSomething = true;
2413
+ source += `(?:${options.map(escapeForPattern).join("|")})`;
2414
+ }
2415
+ return readSomething ? new RegExp(`${source}$`) : null;
2416
+ }
2417
+ function escapeForPattern(text2) {
2418
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2419
+ }
2420
+ function loopTurns(db, rootsByFile, nameKey2) {
2421
+ let turns = [/* @__PURE__ */ new Map()];
2422
+ for (const over of listsBehind(db, rootsByFile, nameKey2)) {
2253
2423
  turns = turns.flatMap(
2254
- (base) => perElement.map((element) => new Map([...base, ...element]))
2424
+ (base) => over.elements.map((value, position) => {
2425
+ const binding = new Map([...base, [over.element, value]]);
2426
+ if (over.index !== "") {
2427
+ binding.set(over.index, String(position));
2428
+ }
2429
+ return binding;
2430
+ })
2255
2431
  );
2256
2432
  }
2257
2433
  return turns;
2258
2434
  }
2259
- function blocksAround(call) {
2260
- const blocks = [];
2261
- let current = call.parent;
2262
- while (current !== null && !OWN_BODY_TYPES.has(current.type)) {
2263
- const parent = current.parent;
2264
- if (BLOCK_TYPES3.has(current.type) && parent?.type === "call") {
2265
- blocks.unshift({ block: current, call: parent });
2435
+ function listsBehind(db, rootsByFile, nameKey2) {
2436
+ const found = [];
2437
+ for (const [, element, index, overKey] of db.lookup(
2438
+ "nameTurnsOn",
2439
+ 0,
2440
+ nameKey2
2441
+ )) {
2442
+ const elements = literalElementsOf(rootsByFile, String(overKey), db);
2443
+ if (elements !== null) {
2444
+ found.push({ element: String(element), index: String(index), elements });
2266
2445
  }
2267
- current = parent;
2268
2446
  }
2269
- return blocks;
2270
- }
2271
- function elementBindings(taken) {
2272
- const { block, call } = taken;
2273
- const receiver = field(call, "receiver");
2274
- const method = field(call, "method")?.text;
2275
- if (receiver === null || method === void 0 || !LOOP_METHODS.has(method)) {
2276
- return null;
2277
- }
2278
- const parameters = field(block, "parameters");
2279
- const [element, index] = parameters === null ? [] : bodyStatements(parameters).map((parameter) => parameter.text);
2280
- const elements = element === void 0 ? null : literalElementsOf(receiver);
2281
- if (element === void 0 || elements === null) {
2282
- return null;
2283
- }
2284
- return elements.map((value, position) => {
2285
- const binding = /* @__PURE__ */ new Map([[element, value]]);
2286
- if (index !== void 0) {
2287
- binding.set(index, String(position));
2288
- }
2289
- return binding;
2290
- });
2447
+ return found;
2291
2448
  }
2292
- function literalElementsOf(node) {
2293
- const value = evaluatedValue(node);
2294
- if (value.kind !== "sequence") {
2449
+ function literalElementsOf(rootsByFile, key, db) {
2450
+ const node = (0, import_resolution5.nodeOfKey)(rootsByFile, key);
2451
+ const value = node === null ? null : evaluatedValue(node, db);
2452
+ if (value === null || value.kind !== "sequence") {
2295
2453
  return null;
2296
2454
  }
2297
2455
  const elements = [];
@@ -2431,8 +2589,6 @@ function graphqlTypeNameFromQualified(qualifiedName, convention) {
2431
2589
  }
2432
2590
 
2433
2591
  // src/ancestry.ts
2434
- var INCLUDE_CALL = "include";
2435
- var PREPEND_CALL = "prepend";
2436
2592
  async function reachDefinition(qualifiedName, lookup) {
2437
2593
  const filePath = resolveConstantFile(
2438
2594
  lookup.root,
@@ -2559,7 +2715,7 @@ function superclassCandidatesOf(blocks) {
2559
2715
  }
2560
2716
  return [];
2561
2717
  }
2562
- function methodInAncestry(ancestry, name) {
2718
+ function methodInAncestry(ancestry, name, read = {}) {
2563
2719
  for (const entry of ancestry) {
2564
2720
  if (entry.type === "root") {
2565
2721
  return { type: "none" };
@@ -2571,7 +2727,7 @@ function methodInAncestry(ancestry, name) {
2571
2727
  cause: "unreadAncestor"
2572
2728
  };
2573
2729
  }
2574
- const found = definitionIn(entry.blocks, name);
2730
+ const found = definitionIn(entry.blocks, name, read);
2575
2731
  if (found.method !== null && found.block !== null) {
2576
2732
  return { type: "found", method: found.method, block: found.block };
2577
2733
  }
@@ -2585,7 +2741,7 @@ function methodInAncestry(ancestry, name) {
2585
2741
  }
2586
2742
  return { type: "none" };
2587
2743
  }
2588
- function definitionIn(blocks, name) {
2744
+ function definitionIn(blocks, name, read) {
2589
2745
  let method = null;
2590
2746
  let block = null;
2591
2747
  let definedDynamically = false;
@@ -2595,14 +2751,17 @@ function definitionIn(blocks, name) {
2595
2751
  if (body === null) {
2596
2752
  continue;
2597
2753
  }
2598
- const found = instanceMethodsByName(body).get(name);
2754
+ const found = instanceMethodsByName(body, read.bodyBlocks).get(name);
2599
2755
  if (found !== void 0) {
2600
2756
  method = found;
2601
2757
  block = candidate;
2602
2758
  }
2603
- const defined = defineMethodNames(body);
2759
+ const defined = definedNamesOf(
2760
+ read.dynamicNames,
2761
+ nodeId(candidate.file, candidate.info.node)
2762
+ );
2604
2763
  definedDynamically ||= defined.names.has(name);
2605
- unreadableDefine ||= defined.unreadable;
2764
+ unreadableDefine ||= couldBeDefined(defined, name);
2606
2765
  }
2607
2766
  return { method, block, definedDynamically, unreadableDefine };
2608
2767
  }
@@ -2631,7 +2790,7 @@ var import_extractor5 = require("@suss/extractor");
2631
2790
 
2632
2791
  // src/clientCalls.ts
2633
2792
  var import_behavioral_ir = require("@suss/behavioral-ir");
2634
- var import_values6 = require("@suss/values");
2793
+ var import_values7 = require("@suss/values");
2635
2794
 
2636
2795
  // src/paths/effects.ts
2637
2796
  var import_extractor2 = require("@suss/extractor");
@@ -3012,7 +3171,7 @@ function lowerRubyBody(body, terminals, responseCalls = []) {
3012
3171
 
3013
3172
  // src/paths/effects.ts
3014
3173
  var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
3015
- var BLOCK_TYPES4 = /* @__PURE__ */ new Set([
3174
+ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
3016
3175
  "body_statement",
3017
3176
  "then",
3018
3177
  "else",
@@ -3141,7 +3300,7 @@ function argsOf(call) {
3141
3300
  function enclosingStatement(call, body) {
3142
3301
  let current = call;
3143
3302
  while (current.parent !== null) {
3144
- if (BLOCK_TYPES4.has(current.parent.type) || current.parent.id === body.id) {
3303
+ if (BLOCK_TYPES5.has(current.parent.type) || current.parent.id === body.id) {
3145
3304
  return current;
3146
3305
  }
3147
3306
  current = current.parent;
@@ -3197,7 +3356,7 @@ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, kee
3197
3356
 
3198
3357
  // src/responseStatus.ts
3199
3358
  var import_extractor3 = require("@suss/extractor");
3200
- var import_values5 = require("@suss/values");
3359
+ var import_values6 = require("@suss/values");
3201
3360
 
3202
3361
  // src/paths/predicates.ts
3203
3362
  var OPERATORS = {
@@ -3324,11 +3483,11 @@ function statusArgumentOf(call, declaration) {
3324
3483
  }
3325
3484
  function statusNumberOf(node, names) {
3326
3485
  const value = evaluatedValue(node);
3327
- const constant2 = (0, import_values5.constantOf)(value);
3486
+ const constant2 = (0, import_values6.constantOf)(value);
3328
3487
  if (typeof constant2 === "number") {
3329
3488
  return constant2;
3330
3489
  }
3331
- const name = (0, import_values5.literalOf)(value);
3490
+ const name = (0, import_values6.literalOf)(value);
3332
3491
  return name === null ? null : names[name] ?? null;
3333
3492
  }
3334
3493
  function declarationsByName(declarations) {
@@ -3649,7 +3808,7 @@ function receiverPrefix(receiver, method, root, pattern, options) {
3649
3808
  const args = readCallArgs(field(built, "arguments"));
3650
3809
  const keyword = pattern.builderUrlKeyword;
3651
3810
  const base = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[0];
3652
- const path7 = base === void 0 ? null : (0, import_values6.pathOf)(evaluatedValue(base, options.facts));
3811
+ const path7 = base === void 0 ? null : (0, import_values7.pathOf)(evaluatedValue(base, options.facts));
3653
3812
  return path7 === void 0 || path7 === null ? "" : trimmed(path7);
3654
3813
  }
3655
3814
  function trimmed(path7) {
@@ -3715,7 +3874,7 @@ function pathAt(written, options) {
3715
3874
  if (written === void 0) {
3716
3875
  return null;
3717
3876
  }
3718
- const path7 = (0, import_values6.pathOf)(evaluatedValue(written, options.facts));
3877
+ const path7 = (0, import_values7.pathOf)(evaluatedValue(written, options.facts));
3719
3878
  return path7 === void 0 || (0, import_behavioral_ir.namesNothing)(path7) ? null : path7;
3720
3879
  }
3721
3880
  function clientUnit(method, name, request, pattern, pack, options) {
@@ -3884,7 +4043,7 @@ function isDefaultedAt(node) {
3884
4043
  }
3885
4044
 
3886
4045
  // src/filters.ts
3887
- function controllerFilters(pattern, ancestry) {
4046
+ function controllerFilters(pattern, ancestry, read = {}) {
3888
4047
  const forms = pattern.filters ?? [];
3889
4048
  if (forms.length === 0) {
3890
4049
  return [];
@@ -3919,7 +4078,7 @@ function controllerFilters(pattern, ancestry) {
3919
4078
  }
3920
4079
  const resolved = [];
3921
4080
  for (const declaration of declared) {
3922
- const found = methodInAncestry(ancestry, declaration.methodName);
4081
+ const found = methodInAncestry(ancestry, declaration.methodName, read);
3923
4082
  if (found.type !== "found") {
3924
4083
  continue;
3925
4084
  }
@@ -4077,7 +4236,7 @@ function namesUnder(node) {
4077
4236
 
4078
4237
  // src/storage.ts
4079
4238
  var import_ir_core = require("@suss/ir-core");
4080
- var import_resolution5 = require("@suss/resolution");
4239
+ var import_resolution6 = require("@suss/resolution");
4081
4240
  function children3(node) {
4082
4241
  return node.namedChildren.filter((child) => child !== null);
4083
4242
  }
@@ -4104,7 +4263,7 @@ function constantName(constant2) {
4104
4263
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
4105
4264
  }
4106
4265
  function reachesBase(facts, classKey, bases) {
4107
- (0, import_resolution5.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4266
+ (0, import_resolution6.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4108
4267
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4109
4268
  }
4110
4269
  function classBehind(facts, file, constant2) {
@@ -4123,28 +4282,98 @@ function modelPattern(constant2, file, options) {
4123
4282
  (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
4124
4283
  );
4125
4284
  }
4126
- function selectorOf(node) {
4127
- const picked = [];
4128
- const walk = (current) => {
4129
- const args = field(current, "arguments");
4130
- for (const argument of args === null ? [] : children3(args)) {
4131
- if (argument.type !== "pair") {
4132
- continue;
4133
- }
4134
- const key = field(argument, "key");
4285
+ function chainLinks(call) {
4286
+ const links = [];
4287
+ let current = call;
4288
+ while (current !== null && current.type === "call") {
4289
+ links.push(current);
4290
+ current = receiverOf(current);
4291
+ }
4292
+ return links;
4293
+ }
4294
+ function kindOfCall(pattern, method) {
4295
+ if (pattern.writes.includes(method)) {
4296
+ return "write";
4297
+ }
4298
+ if (pattern.reads.includes(method)) {
4299
+ return "read";
4300
+ }
4301
+ return void 0;
4302
+ }
4303
+ function someLibraryDefines(options, method) {
4304
+ return options.patterns.some(
4305
+ (pattern) => kindOfCall(pattern, method) !== void 0
4306
+ );
4307
+ }
4308
+ function argumentsOf2(call) {
4309
+ const args = field(call, "arguments");
4310
+ return args === null ? [] : children3(args);
4311
+ }
4312
+ function isKeywordArgument(argument) {
4313
+ return argument.type === "pair" || argument.type === "hash";
4314
+ }
4315
+ function bareName(node) {
4316
+ return node.text.replace(/^:/, "").replace(/:$/, "");
4317
+ }
4318
+ function pairsIn(argument) {
4319
+ if (argument.type === "pair") {
4320
+ return [argument];
4321
+ }
4322
+ if (argument.type === "hash") {
4323
+ return children3(argument).filter((child) => child.type === "pair");
4324
+ }
4325
+ return [];
4326
+ }
4327
+ function keywordKeys(call) {
4328
+ const keys = [];
4329
+ for (const argument of argumentsOf2(call)) {
4330
+ for (const pair of pairsIn(argument)) {
4331
+ const key = field(pair, "key");
4135
4332
  if (key !== null) {
4136
- picked.push(key.text.replace(/^:/, "").replace(/:$/, ""));
4333
+ keys.push(bareName(key));
4137
4334
  }
4138
4335
  }
4139
- const receiver = receiverOf(current);
4140
- if (receiver !== null) {
4141
- walk(receiver);
4336
+ }
4337
+ return keys;
4338
+ }
4339
+ function hasPositionalArgument(call) {
4340
+ return argumentsOf2(call).some((argument) => !isKeywordArgument(argument));
4341
+ }
4342
+ function columnsAskedFor(call, pattern) {
4343
+ if (!(pattern.columnArguments ?? []).includes(methodOf(call))) {
4344
+ return [];
4345
+ }
4346
+ return argumentsOf2(call).filter((argument) => argument.type === "simple_symbol").map(bareName);
4347
+ }
4348
+ function primaryKeySelector(call, pattern) {
4349
+ const byPrimaryKey = pattern.byPrimaryKey;
4350
+ if (byPrimaryKey === void 0) {
4351
+ return [];
4352
+ }
4353
+ const picks = byPrimaryKey.methods.includes(methodOf(call)) && hasPositionalArgument(call);
4354
+ return picks ? [byPrimaryKey.column] : [];
4355
+ }
4356
+ function selectorOf(call, pattern) {
4357
+ const picked = [];
4358
+ for (const link of chainLinks(call)) {
4359
+ picked.push(...primaryKeySelector(link, pattern));
4360
+ if (pattern.reads.includes(methodOf(link))) {
4361
+ picked.push(...keywordKeys(link));
4142
4362
  }
4143
- };
4144
- walk(node);
4363
+ }
4145
4364
  return [...new Set(picked)];
4146
4365
  }
4147
- function storageEffect(call, container, pattern, kind, selector) {
4366
+ function fieldsOf(call, pattern, kind) {
4367
+ if (kind === "write") {
4368
+ return [...new Set(keywordKeys(call))];
4369
+ }
4370
+ return [
4371
+ ...new Set(
4372
+ chainLinks(call).flatMap((link) => columnsAskedFor(link, pattern))
4373
+ )
4374
+ ];
4375
+ }
4376
+ function storageEffect(call, container, pattern, kind, selector, fields) {
4148
4377
  const operation = methodOf(call);
4149
4378
  return {
4150
4379
  type: "interaction",
@@ -4158,37 +4387,14 @@ function storageEffect(call, container, pattern, kind, selector) {
4158
4387
  interaction: {
4159
4388
  class: "storage-access",
4160
4389
  kind,
4161
- fields: [],
4390
+ fields,
4162
4391
  operation,
4163
4392
  ...selector.length > 0 ? { selector } : {}
4164
4393
  }
4165
4394
  };
4166
4395
  }
4167
- function modelCallEffects(call, file, options) {
4168
- const constant2 = rootConstant(call);
4169
- if (constant2 === null) {
4170
- return [];
4171
- }
4172
- const pattern = modelPattern(constant2, file, options);
4173
- if (pattern === void 0) {
4174
- return [];
4175
- }
4176
- const kind = pattern.writes.includes(methodOf(call)) ? "write" : "read";
4177
- return [
4178
- storageEffect(
4179
- call,
4180
- constantName(constant2),
4181
- pattern,
4182
- kind,
4183
- selectorOf(call)
4184
- )
4185
- ];
4186
- }
4187
- function writeMethods(patterns) {
4188
- return new Set(patterns.flatMap((pattern) => pattern.writes));
4189
- }
4190
4396
  function classSettledOn(facts, key) {
4191
- (0, import_resolution5.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4397
+ (0, import_resolution6.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4192
4398
  const objects = new Set(
4193
4399
  facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
4194
4400
  );
@@ -4203,27 +4409,64 @@ function declaredName(facts, classKey) {
4203
4409
  function projectDeclares(facts, classKey, method) {
4204
4410
  return facts.lookup("wantedDeclaredName", 0, classKey).some((row) => String(row[1]) === method);
4205
4411
  }
4206
- function instanceWriteEffects(call, file, options, enclosing) {
4412
+ function constantReceiver(constant2, file, facts) {
4413
+ const classKey = classBehind(facts, file, constant2);
4414
+ return classKey === void 0 ? void 0 : { classKey, container: constantName(constant2) };
4415
+ }
4416
+ function settledReceiver(receiver, file, facts, enclosing) {
4417
+ const classKey = classSettledOn(facts, readKey(file, receiver, enclosing));
4418
+ if (classKey === void 0) {
4419
+ return void 0;
4420
+ }
4421
+ const container = declaredName(facts, classKey);
4422
+ return container === void 0 ? void 0 : { classKey, container };
4423
+ }
4424
+ function receiverClass(call, file, options, enclosing) {
4425
+ const constant2 = rootConstant(call);
4426
+ if (constant2 !== null) {
4427
+ return constantReceiver(constant2, file, options.facts);
4428
+ }
4207
4429
  const receiver = receiverOf(call);
4208
- const method = methodOf(call);
4209
- if (receiver === null || !writeMethods(options.patterns).has(method)) {
4210
- return [];
4430
+ if (receiver === null) {
4431
+ return void 0;
4211
4432
  }
4212
- const classKey = classSettledOn(
4213
- options.facts,
4214
- readKey(file, receiver, enclosing)
4215
- );
4216
- if (classKey === void 0) {
4433
+ return settledReceiver(receiver, file, options.facts, enclosing);
4434
+ }
4435
+ function libraryCallIn(call, options) {
4436
+ return chainLinks(call).find(
4437
+ (link) => someLibraryDefines(options, methodOf(link))
4438
+ ) ?? null;
4439
+ }
4440
+ function modelCallEffects(call, file, options, enclosing) {
4441
+ const worked = libraryCallIn(call, options);
4442
+ if (worked === null) {
4217
4443
  return [];
4218
4444
  }
4219
- const pattern = options.patterns.find(
4220
- (candidate) => candidate.writes.includes(method) && reachesBase(options.facts, classKey, candidate.baseClasses)
4221
- );
4222
- if (pattern === void 0 || projectDeclares(options.facts, classKey, method)) {
4445
+ const method = methodOf(worked);
4446
+ const target = receiverClass(worked, file, options, enclosing);
4447
+ if (target === void 0) {
4223
4448
  return [];
4224
4449
  }
4225
- const container = declaredName(options.facts, classKey);
4226
- return container === void 0 ? [] : [storageEffect(call, container, pattern, "write", [])];
4450
+ for (const pattern of options.patterns) {
4451
+ const kind = kindOfCall(pattern, method);
4452
+ if (kind === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
4453
+ continue;
4454
+ }
4455
+ if (projectDeclares(options.facts, target.classKey, method)) {
4456
+ return [];
4457
+ }
4458
+ return [
4459
+ storageEffect(
4460
+ worked,
4461
+ target.container,
4462
+ pattern,
4463
+ kind,
4464
+ selectorOf(worked, pattern),
4465
+ fieldsOf(worked, pattern, kind)
4466
+ )
4467
+ ];
4468
+ }
4469
+ return [];
4227
4470
  }
4228
4471
  function isLoader(node, loader) {
4229
4472
  if (node === null) {
@@ -4261,7 +4504,7 @@ function loaderCallEffects(call, file, options) {
4261
4504
  const pattern = modelPattern(argument, file, options);
4262
4505
  if (pattern !== void 0) {
4263
4506
  effects.push(
4264
- storageEffect(call, constantName(argument), pattern, "read", [])
4507
+ storageEffect(call, constantName(argument), pattern, "read", [], [])
4265
4508
  );
4266
4509
  }
4267
4510
  }
@@ -4269,22 +4512,21 @@ function loaderCallEffects(call, file, options) {
4269
4512
  return effects;
4270
4513
  }
4271
4514
  function effectsOfCall(call, file, options, enclosing) {
4272
- const onModel = modelCallEffects(call, file, options);
4273
- if (onModel.length > 0) {
4274
- return onModel;
4275
- }
4276
- const onInstance = rootConstant(call) === null ? instanceWriteEffects(call, file, options, enclosing) : [];
4277
- return onInstance.length > 0 ? onInstance : loaderCallEffects(call, file, options);
4515
+ const onModel = modelCallEffects(call, file, options, enclosing);
4516
+ return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
4278
4517
  }
4279
4518
  function storageClaims(call, file, options, enclosing = null) {
4280
4519
  return options.patterns.length > 0 && effectsOfCall(call, file, options, enclosing).length > 0;
4281
4520
  }
4282
4521
  function receiverKeysToAsk(chains, file, options, enclosing) {
4283
- const writes = writeMethods(options.patterns);
4284
4522
  const keys = [];
4285
4523
  for (const call of chains) {
4286
- const receiver = receiverOf(call);
4287
- if (receiver !== null && writes.has(methodOf(call)) && rootConstant(call) === null) {
4524
+ const worked = libraryCallIn(call, options);
4525
+ if (worked === null || rootConstant(worked) !== null) {
4526
+ continue;
4527
+ }
4528
+ const receiver = receiverOf(worked);
4529
+ if (receiver !== null) {
4288
4530
  keys.push(readKey(file, receiver, enclosing));
4289
4531
  }
4290
4532
  }
@@ -4302,7 +4544,7 @@ function storageEffects(calls, file, options, enclosing = null) {
4302
4544
  }
4303
4545
  }
4304
4546
  const chains = calls.filter((call) => !partOfOne.has(call.id));
4305
- (0, import_resolution5.askResolution)(
4547
+ (0, import_resolution6.askResolution)(
4306
4548
  options.facts,
4307
4549
  receiverKeysToAsk(chains, file, options, enclosing),
4308
4550
  "wanted",
@@ -4381,11 +4623,17 @@ function createFileCache(parse, readFile) {
4381
4623
  }
4382
4624
  };
4383
4625
  }
4384
- function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
4626
+ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
4385
4627
  return {
4386
4628
  pattern,
4387
4629
  cache,
4388
4630
  bodyRead,
4631
+ facts,
4632
+ read: {
4633
+ facts,
4634
+ bodyBlocks: bodyRead.bodyBlocks,
4635
+ dynamicNames: bodyRead.dynamicNames
4636
+ },
4389
4637
  lookup: {
4390
4638
  root: pattern.root,
4391
4639
  pathConvention: pattern.pathConvention,
@@ -4468,7 +4716,13 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
4468
4716
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
4469
4717
  return [];
4470
4718
  }
4471
- const ctx = fieldReadContext(pattern, options.cache, fileBlocks, options);
4719
+ const ctx = fieldReadContext(
4720
+ pattern,
4721
+ options.cache,
4722
+ fileBlocks,
4723
+ options,
4724
+ options.facts
4725
+ );
4472
4726
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
4473
4727
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
4474
4728
  return [];
@@ -4516,7 +4770,11 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4516
4770
  ])) {
4517
4771
  return [];
4518
4772
  }
4519
- const filters = controllerFilters(pattern, ancestry);
4773
+ const filters = controllerFilters(pattern, ancestry, {
4774
+ facts: options.facts,
4775
+ bodyBlocks: options.bodyBlocks,
4776
+ dynamicNames: options.dynamicNames
4777
+ });
4520
4778
  const units = [];
4521
4779
  for (const filter of filters) {
4522
4780
  const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
@@ -4561,7 +4819,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4561
4819
  });
4562
4820
  };
4563
4821
  const own = /* @__PURE__ */ new Set();
4564
- for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
4822
+ for (const [actionName, method, block] of publicInstanceMethods(
4823
+ ownBlocks,
4824
+ options.bodyBlocks
4825
+ )) {
4565
4826
  own.add(actionName);
4566
4827
  emitAction(actionName, method, block);
4567
4828
  }
@@ -4571,7 +4832,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4571
4832
  continue;
4572
4833
  }
4573
4834
  for (const [actionName, method, block] of publicInstanceMethods(
4574
- entry.blocks
4835
+ entry.blocks,
4836
+ options.bodyBlocks
4575
4837
  )) {
4576
4838
  if (seen.has(actionName)) {
4577
4839
  continue;
@@ -4585,14 +4847,17 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4585
4847
  }
4586
4848
  return units;
4587
4849
  }
4588
- function publicInstanceMethods(blocks) {
4850
+ function publicInstanceMethods(blocks, bodyBlocks) {
4589
4851
  const found = [];
4590
4852
  for (const block of blocks) {
4591
4853
  if (block.info.bodyNode === null) {
4592
4854
  continue;
4593
4855
  }
4594
4856
  const visibility = instanceMethodVisibility(block.info.bodyNode);
4595
- for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
4857
+ for (const [name, method] of instanceMethodsByName(
4858
+ block.info.bodyNode,
4859
+ bodyBlocks
4860
+ )) {
4596
4861
  if ((visibility.get(name) ?? "public") === "public") {
4597
4862
  found.push([name, method, block]);
4598
4863
  }
@@ -4855,7 +5120,7 @@ async function readFieldShape(symbol, callArgs, scope, ctx, ancestry, range) {
4855
5120
  return {
4856
5121
  contract: literalContract(callArgs, scope, ctx),
4857
5122
  body: bodyFromLookup(
4858
- methodInAncestry(ancestry, symbol),
5123
+ methodInAncestry(ancestry, symbol, ctx.read),
4859
5124
  range,
4860
5125
  "This field",
4861
5126
  NO_METHOD_BEHIND_IT,
@@ -4900,7 +5165,7 @@ async function readWiredClass(ref, scope, ctx) {
4900
5165
  return {
4901
5166
  contract: readClassContract(ancestry, ctx.pattern),
4902
5167
  body: bodyFromLookup(
4903
- methodInAncestry(ancestry, ctx.pattern.resolverMethodName),
5168
+ methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
4904
5169
  range,
4905
5170
  `This field's ${targetQualifiedName}`,
4906
5171
  methodNotSettled(
@@ -5072,6 +5337,202 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
5072
5337
  };
5073
5338
  }
5074
5339
 
5340
+ // src/inflect.ts
5341
+ var SINGULAR_RULES = [
5342
+ [/s$/i, ""],
5343
+ [/(ss)$/i, "$1"],
5344
+ [/(n)ews$/i, "$1ews"],
5345
+ [/([ti])a$/i, "$1um"],
5346
+ [
5347
+ /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i,
5348
+ "$1sis"
5349
+ ],
5350
+ [/(^analy)(sis|ses)$/i, "$1sis"],
5351
+ [/([^f])ves$/i, "$1fe"],
5352
+ [/(hive)s$/i, "$1"],
5353
+ [/(tive)s$/i, "$1"],
5354
+ [/([lr])ves$/i, "$1f"],
5355
+ [/([^aeiouy]|qu)ies$/i, "$1y"],
5356
+ [/(s)eries$/i, "$1eries"],
5357
+ [/(m)ovies$/i, "$1ovie"],
5358
+ [/(x|ch|ss|sh)es$/i, "$1"],
5359
+ [/^(m|l)ice$/i, "$1ouse"],
5360
+ [/(bus)(es)?$/i, "$1"],
5361
+ [/(o)es$/i, "$1"],
5362
+ [/(shoe)s$/i, "$1"],
5363
+ [/(cris|test)(is|es)$/i, "$1is"],
5364
+ [/^(a)x[ie]s$/i, "$1xis"],
5365
+ [/(octop|vir)(us|i)$/i, "$1us"],
5366
+ [/(alias|status)(es)?$/i, "$1"],
5367
+ [/^(ox)en/i, "$1"],
5368
+ [/(vert|ind)ices$/i, "$1ex"],
5369
+ [/(matr)ices$/i, "$1ix"],
5370
+ [/(quiz)zes$/i, "$1"],
5371
+ [/(database)s$/i, "$1"],
5372
+ // The words ActiveSupport calls irregular. It declares them after the
5373
+ // rules above, which is what puts them ahead in the search.
5374
+ [/(p)erson$/i, "$1erson"],
5375
+ [/(p)eople$/i, "$1erson"],
5376
+ [/(m)an$/i, "$1an"],
5377
+ [/(m)en$/i, "$1an"],
5378
+ [/(c)hild$/i, "$1hild"],
5379
+ [/(c)hildren$/i, "$1hild"],
5380
+ [/(s)ex$/i, "$1ex"],
5381
+ [/(s)exes$/i, "$1ex"],
5382
+ [/(m)ove$/i, "$1ove"],
5383
+ [/(m)oves$/i, "$1ove"],
5384
+ [/(z)ombie$/i, "$1ombie"],
5385
+ [/(z)ombies$/i, "$1ombie"]
5386
+ ];
5387
+ var UNCOUNTABLE = /* @__PURE__ */ new Set([
5388
+ "equipment",
5389
+ "information",
5390
+ "rice",
5391
+ "money",
5392
+ "species",
5393
+ "series",
5394
+ "fish",
5395
+ "sheep",
5396
+ "jeans",
5397
+ "police"
5398
+ ]);
5399
+ function singularize(word, project) {
5400
+ if (UNCOUNTABLE.has(word.toLowerCase()) || (project?.uncountable ?? []).some(
5401
+ (uncountable) => uncountable.toLowerCase() === word.toLowerCase()
5402
+ )) {
5403
+ return word;
5404
+ }
5405
+ const own = projectSingular(word, project);
5406
+ if (own !== null) {
5407
+ return own;
5408
+ }
5409
+ for (let at = SINGULAR_RULES.length - 1; at >= 0; at--) {
5410
+ const [pattern, replacement] = SINGULAR_RULES[at];
5411
+ if (pattern.test(word)) {
5412
+ return word.replace(pattern, replacement);
5413
+ }
5414
+ }
5415
+ return word;
5416
+ }
5417
+ function projectSingular(word, project) {
5418
+ const irregular = [...project?.irregular ?? []].reverse();
5419
+ for (const [plural, singular] of irregular) {
5420
+ if (plural.toLowerCase() === word.toLowerCase()) {
5421
+ return matchingCase(word, singular);
5422
+ }
5423
+ }
5424
+ const rules = [...project?.singular ?? []].reverse();
5425
+ for (const [rule3, replacement] of rules) {
5426
+ const applied = applyRule(word, rule3, replacement);
5427
+ if (applied !== null) {
5428
+ return applied;
5429
+ }
5430
+ }
5431
+ return null;
5432
+ }
5433
+ function applyRule(word, rule3, replacement) {
5434
+ const written = /^\/(.*)\/([a-z]*)$/s.exec(rule3);
5435
+ if (written === null) {
5436
+ return word.includes(rule3) ? word.replace(rule3, replacement) : null;
5437
+ }
5438
+ const flags = (written[2] ?? "").replace("m", "s").replace(/[^is]/g, "");
5439
+ const pattern = new RegExp(written[1] ?? "", flags);
5440
+ return pattern.test(word) ? word.replace(pattern, replacement.replace(/\\(\d)/g, "$$$1")) : null;
5441
+ }
5442
+ function matchingCase(word, replacement) {
5443
+ const first = word[0];
5444
+ if (first === void 0 || first !== first.toUpperCase()) {
5445
+ return replacement;
5446
+ }
5447
+ return replacement.charAt(0).toUpperCase() + replacement.slice(1);
5448
+ }
5449
+ function camelize(word, acronyms = []) {
5450
+ return word.split("_").map((part) => capitalize(part, acronyms)).join("");
5451
+ }
5452
+ function capitalize(part, acronyms) {
5453
+ if (part === "") {
5454
+ return "";
5455
+ }
5456
+ const acronym = acronyms.find(
5457
+ (known) => known.toLowerCase() === part.toLowerCase()
5458
+ );
5459
+ return acronym ?? part[0]?.toUpperCase() + part.slice(1);
5460
+ }
5461
+ function associationTargetName(name, plural, project) {
5462
+ return camelize(
5463
+ plural ? singularize(name, project) : name,
5464
+ project?.acronyms ?? []
5465
+ );
5466
+ }
5467
+
5468
+ // src/facts/associations.ts
5469
+ function declaredName2(argument) {
5470
+ if (argument === void 0) {
5471
+ return null;
5472
+ }
5473
+ if (argument.type === "simple_symbol") {
5474
+ return argument.text.slice(1);
5475
+ }
5476
+ return stringLiteralValue(argument);
5477
+ }
5478
+ function targetReference(key, written, nesting) {
5479
+ if (written.startsWith("::")) {
5480
+ return { key, written: written.slice(2), nesting: [] };
5481
+ }
5482
+ return { key, written, nesting };
5483
+ }
5484
+ function targetName(call, calls, name, plural, inflections) {
5485
+ const override = readCallArgs(field(call, "arguments")).keyword[calls.classNameKeyword];
5486
+ if (override === void 0) {
5487
+ return associationTargetName(name, plural, inflections);
5488
+ }
5489
+ return stringLiteralValue(override);
5490
+ }
5491
+ function associationsDeclaredIn(cls, classKey, nesting, calls, inflections) {
5492
+ const body = field(cls, "body");
5493
+ if (body === null || calls.length === 0) {
5494
+ return [];
5495
+ }
5496
+ const found = [];
5497
+ for (const statement of runStatements(body)) {
5498
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
5499
+ continue;
5500
+ }
5501
+ const method = field(statement, "method")?.text ?? "";
5502
+ for (const declaration of calls) {
5503
+ const plural = declaration.plural.includes(method);
5504
+ if (!plural && !declaration.singular.includes(method)) {
5505
+ continue;
5506
+ }
5507
+ const positional = readCallArgs(field(statement, "arguments")).positional;
5508
+ const name = declaredName2(positional[0]);
5509
+ if (name === null) {
5510
+ continue;
5511
+ }
5512
+ const written = targetName(
5513
+ statement,
5514
+ declaration,
5515
+ name,
5516
+ plural,
5517
+ inflections
5518
+ );
5519
+ if (written === null) {
5520
+ continue;
5521
+ }
5522
+ found.push({
5523
+ classKey,
5524
+ name,
5525
+ target: targetReference(
5526
+ `${classKey}#association:${name}`,
5527
+ written,
5528
+ nesting
5529
+ )
5530
+ });
5531
+ }
5532
+ }
5533
+ return found;
5534
+ }
5535
+
5075
5536
  // src/facts/constants.ts
5076
5537
  function children4(node) {
5077
5538
  return node.namedChildren.filter((child) => child !== null);
@@ -5106,20 +5567,32 @@ function isDeclaration(node, parent) {
5106
5567
  }
5107
5568
  return parent.type === "scope_resolution";
5108
5569
  }
5109
- function collectFileConstants(filePath, root) {
5570
+ function collectFileConstants(filePath, root, associationCalls = [], inflections) {
5110
5571
  const definitions = [];
5111
5572
  const references = [];
5573
+ const associations = [];
5112
5574
  const walk = (node, parent, nesting) => {
5113
5575
  if (NESTING_TYPES.has(node.type)) {
5114
5576
  const name = field(node, "name");
5115
5577
  const written = name === null ? null : writtenName(name);
5116
5578
  if (written !== null) {
5579
+ const classKey = nodeId(filePath, node);
5117
5580
  definitions.push({
5118
5581
  qualifiedName: qualify(nesting, written),
5119
- key: nodeId(filePath, node),
5582
+ key: classKey,
5120
5583
  kind: "declaration"
5121
5584
  });
5122
5585
  const inside = [...nesting, ...written.split("::")];
5586
+ for (const declared of associationsDeclaredIn(
5587
+ node,
5588
+ classKey,
5589
+ inside,
5590
+ associationCalls,
5591
+ inflections
5592
+ )) {
5593
+ associations.push(declared);
5594
+ references.push(declared.target);
5595
+ }
5123
5596
  for (const child of children4(node)) {
5124
5597
  walk(child, node, inside);
5125
5598
  }
@@ -5151,7 +5624,7 @@ function collectFileConstants(filePath, root) {
5151
5624
  }
5152
5625
  };
5153
5626
  walk(root, null, []);
5154
- return { filePath, definitions, references };
5627
+ return { filePath, definitions, references, associations };
5155
5628
  }
5156
5629
  function emitConstantBindings(db, perFile) {
5157
5630
  const byName = /* @__PURE__ */ new Map();
@@ -5175,6 +5648,13 @@ function emitConstantBindings(db, perFile) {
5175
5648
  return found.length === 1 ? found[0].key : null;
5176
5649
  };
5177
5650
  for (const file of files) {
5651
+ for (const declared of file.associations) {
5652
+ db.add("declaresAssociation", [
5653
+ declared.classKey,
5654
+ declared.name,
5655
+ declared.target.key
5656
+ ]);
5657
+ }
5178
5658
  for (const reference of file.references) {
5179
5659
  for (let depth = reference.nesting.length; depth >= 0; depth--) {
5180
5660
  const candidate = qualify(
@@ -5199,7 +5679,7 @@ function emitConstantBindings(db, perFile) {
5199
5679
  var import_node_fs4 = __toESM(require("fs"), 1);
5200
5680
  var import_node_path5 = __toESM(require("path"), 1);
5201
5681
  var import_datalog4 = require("@suss/datalog");
5202
- var import_resolution7 = require("@suss/resolution");
5682
+ var import_resolution8 = require("@suss/resolution");
5203
5683
 
5204
5684
  // src/parser.ts
5205
5685
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -5314,13 +5794,42 @@ function requiredRelativePath(node) {
5314
5794
  return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
5315
5795
  }
5316
5796
 
5797
+ // src/pack.ts
5798
+ function inflectionsIn(packs) {
5799
+ const pooled = {
5800
+ acronyms: [],
5801
+ irregular: [],
5802
+ uncountable: [],
5803
+ singular: []
5804
+ };
5805
+ for (const pack of packs) {
5806
+ pooled.acronyms?.push(...pack.inflections?.acronyms ?? []);
5807
+ pooled.irregular?.push(...pack.inflections?.irregular ?? []);
5808
+ pooled.uncountable?.push(...pack.inflections?.uncountable ?? []);
5809
+ pooled.singular?.push(...pack.inflections?.singular ?? []);
5810
+ }
5811
+ return pooled;
5812
+ }
5813
+ function bodyBlocksIn(packs) {
5814
+ const pooled = /* @__PURE__ */ new Map();
5815
+ for (const pack of packs) {
5816
+ for (const declared of pack.bodyBlocks ?? []) {
5817
+ pooled.set(declared.name, {
5818
+ moduleOnly: declared.moduleOnly ?? false,
5819
+ definesClassMethods: declared.definesClassMethods ?? false
5820
+ });
5821
+ }
5822
+ }
5823
+ return pooled;
5824
+ }
5825
+
5317
5826
  // src/reach/closure.ts
5318
5827
  var import_behavioral_ir4 = require("@suss/behavioral-ir");
5319
5828
  var import_datalog2 = require("@suss/datalog");
5320
5829
  var import_extractor8 = require("@suss/extractor");
5321
5830
 
5322
5831
  // src/reach/resolveCallee.ts
5323
- var import_resolution6 = require("@suss/resolution");
5832
+ var import_resolution7 = require("@suss/resolution");
5324
5833
  var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
5325
5834
  var INVOKES_RECEIVER = "call";
5326
5835
  var STOP_FOR = {
@@ -5349,7 +5858,7 @@ function calleeSpellings(calls, ctx) {
5349
5858
  }
5350
5859
  return {
5351
5860
  spellingOf,
5352
- outcomes: (0, import_resolution6.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
5861
+ outcomes: (0, import_resolution7.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
5353
5862
  };
5354
5863
  }
5355
5864
  function resolveCallee(call, site, ctx, read) {
@@ -5363,11 +5872,11 @@ function resolveCallee(call, site, ctx, read) {
5363
5872
  return asCallee(spelling, outcomeFor(spelling.key, ctx, read), site, ctx);
5364
5873
  }
5365
5874
  function outcomeFor(key, ctx, read) {
5366
- return read?.outcomes.get(key) ?? (0, import_resolution6.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
5875
+ return read?.outcomes.get(key) ?? (0, import_resolution7.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
5367
5876
  }
5368
5877
  function mightReadAsACall(call, site, ctx) {
5369
5878
  const spelling = spellingFor(call, site);
5370
- return spelling.kind === "receiver" && (0, import_resolution6.couldBeSettled)(ctx.facts, spelling.key);
5879
+ return spelling.kind === "receiver" && (0, import_resolution7.couldBeSettled)(ctx.facts, spelling.key);
5371
5880
  }
5372
5881
  function readsAsACall(call, site, ctx, read) {
5373
5882
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
@@ -5478,7 +5987,11 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
5478
5987
  if (ancestry === void 0) {
5479
5988
  return stop("outsideRun");
5480
5989
  }
5481
- const found = methodInAncestry(ancestry, methodName);
5990
+ const found = methodInAncestry(ancestry, methodName, {
5991
+ facts: ctx.facts,
5992
+ bodyBlocks: ctx.bodyBlocks,
5993
+ dynamicNames: ctx.dynamicNames
5994
+ });
5482
5995
  if (found.type === "found") {
5483
5996
  return followed(reachedMethod(found.method, found.block, methodName));
5484
5997
  }
@@ -5507,7 +6020,10 @@ function singletonMethodOn(qualifiedName, methodName, ctx) {
5507
6020
  if (block.info.bodyNode === null) {
5508
6021
  continue;
5509
6022
  }
5510
- const found = singletonMethodsByName(block.info.bodyNode).get(methodName);
6023
+ const found = singletonMethodsByName(
6024
+ block.info.bodyNode,
6025
+ ctx.bodyBlocks
6026
+ ).get(methodName);
5511
6027
  if (found !== void 0) {
5512
6028
  return followed(reachedMethod(found, block, methodName));
5513
6029
  }
@@ -5861,7 +6377,7 @@ function parameterName(param) {
5861
6377
 
5862
6378
  // src/reach/context.ts
5863
6379
  var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5864
- async function buildReachContext(files, facts) {
6380
+ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map()) {
5865
6381
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
5866
6382
  const classes = [];
5867
6383
  const classNames = /* @__PURE__ */ new Map();
@@ -5935,7 +6451,9 @@ async function buildReachContext(files, facts) {
5935
6451
  topLevelMethods,
5936
6452
  facts,
5937
6453
  classNames,
5938
- definitions
6454
+ definitions,
6455
+ bodyBlocks,
6456
+ dynamicNames
5939
6457
  };
5940
6458
  }
5941
6459
  function namedChildren(node) {
@@ -5981,6 +6499,13 @@ function inheritedMethodsIn(packs) {
5981
6499
  }
5982
6500
  return found;
5983
6501
  }
6502
+ function associationCallsIn(packs) {
6503
+ return packs.flatMap(
6504
+ (pack) => (pack.storage ?? []).flatMap(
6505
+ (pattern) => pattern.associations === void 0 ? [] : [pattern.associations]
6506
+ )
6507
+ );
6508
+ }
5984
6509
  function emitStorageFacts(db, packs) {
5985
6510
  for (const pack of packs) {
5986
6511
  for (const pattern of pack.storage ?? []) {
@@ -6035,6 +6560,9 @@ async function extractRubyProject(options) {
6035
6560
  );
6036
6561
  const parsed = [];
6037
6562
  const definitions = /* @__PURE__ */ new Map();
6563
+ const associationCalls = associationCallsIn(options.packs);
6564
+ const bodyBlocks = bodyBlocksIn(options.packs);
6565
+ const inflections = inflectionsIn(options.packs);
6038
6566
  for (const file of options.files) {
6039
6567
  await timer.timeAsync("parse", async () => {
6040
6568
  const root = await cache.get(file);
@@ -6042,14 +6570,16 @@ async function extractRubyProject(options) {
6042
6570
  return;
6043
6571
  }
6044
6572
  parsed.push({ file, root });
6045
- emitValueFacts(db, file, root);
6573
+ emitValueFacts(db, file, root, bodyBlocks);
6046
6574
  for (const [key, method] of methodDefinitionsIn(file, root)) {
6047
6575
  definitions.set(key, method);
6048
6576
  }
6049
- constants.push(collectFileConstants(file, root));
6577
+ constants.push(
6578
+ collectFileConstants(file, root, associationCalls, inflections)
6579
+ );
6050
6580
  });
6051
6581
  }
6052
- timer.time("discover", () => {
6582
+ const dynamicNames = timer.time("discover", () => {
6053
6583
  emitConstantBindings(db, constants);
6054
6584
  const known = new Set(parsed.map(({ file }) => file));
6055
6585
  for (const { file, root } of parsed) {
@@ -6057,6 +6587,10 @@ async function extractRubyProject(options) {
6057
6587
  }
6058
6588
  bindEvaluator(db, { files: parsed, definitions });
6059
6589
  emitStorageFacts(db, options.packs);
6590
+ return readDynamicNames(
6591
+ db,
6592
+ new Map(parsed.map(({ file, root }) => [file, root]))
6593
+ );
6060
6594
  });
6061
6595
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
6062
6596
  const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
@@ -6064,7 +6598,7 @@ async function extractRubyProject(options) {
6064
6598
  const inheritedMethods = inheritedMethodsIn(options.packs);
6065
6599
  const reachContext = await timer.timeAsync(
6066
6600
  "discover",
6067
- () => buildReachContext(parsed, db)
6601
+ () => buildReachContext(parsed, db, bodyBlocks, dynamicNames)
6068
6602
  );
6069
6603
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
6070
6604
  const seeds = [];
@@ -6084,6 +6618,8 @@ async function extractRubyProject(options) {
6084
6618
  cache,
6085
6619
  ...storage === void 0 ? {} : { storage },
6086
6620
  inheritedMethods,
6621
+ bodyBlocks,
6622
+ dynamicNames,
6087
6623
  displayPathOf,
6088
6624
  facts: db,
6089
6625
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
@@ -6155,7 +6691,9 @@ async function extractRubyProject(options) {
6155
6691
  context: reachContext,
6156
6692
  displayPathOf,
6157
6693
  ...storage === void 0 ? {} : { storage },
6158
- inheritedMethods
6694
+ inheritedMethods,
6695
+ bodyBlocks,
6696
+ dynamicNames
6159
6697
  })
6160
6698
  );
6161
6699
  for (const entry of found) {
@@ -6272,7 +6810,7 @@ function findRubyFiles(root) {
6272
6810
  }
6273
6811
 
6274
6812
  // src/facts/why.ts
6275
- var WITNESS_RULES = [...import_resolution7.RESOLUTION_RULES, ...RUBY_RULES];
6813
+ var WITNESS_RULES = [...import_resolution8.RESOLUTION_RULES, ...RUBY_RULES];
6276
6814
  var METHOD_TYPES5 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
6277
6815
  function namedChildrenOf(node) {
6278
6816
  return node.namedChildren.filter((child) => child !== null);
@@ -6393,7 +6931,7 @@ var RubyWhySession = class {
6393
6931
  * which the caller says in its own words.
6394
6932
  */
6395
6933
  explain(value, options = {}) {
6396
- return (0, import_resolution7.explainResolvedKey)({
6934
+ return (0, import_resolution8.explainResolvedKey)({
6397
6935
  db: this.db,
6398
6936
  rules: WITNESS_RULES,
6399
6937
  key: readKey(value.file, value.node, enclosingMethod2(value.node)),