@suss/adapter-ruby 0.29.0 → 0.31.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
@@ -44,6 +44,7 @@ __export(index_exports, {
44
44
  emitValueFacts: () => emitValueFacts,
45
45
  evaluatedValue: () => evaluatedValue,
46
46
  extractRubyProject: () => extractRubyProject,
47
+ factsForFile: () => factsForFile,
47
48
  field: () => field,
48
49
  findRubyFiles: () => findRubyFiles,
49
50
  graphqlTypeNameFromQualified: () => graphqlTypeNameFromQualified,
@@ -143,7 +144,52 @@ var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
143
144
  "singleton_class"
144
145
  ]);
145
146
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
147
+ var INCLUDE_CALL = "include";
148
+ var PREPEND_CALL = "prepend";
146
149
  var ARGUMENT_LIST_TYPE = "argument_list";
150
+ var BLOCK_TYPES = /* @__PURE__ */ new Set(["block", "do_block"]);
151
+ var NO_BODY_BLOCKS = /* @__PURE__ */ new Map();
152
+ function isModuleBody(body) {
153
+ return body.parent?.type === "module";
154
+ }
155
+ function declaredBlockAt(statement, isModule, blocks) {
156
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
157
+ return null;
158
+ }
159
+ const method = field(statement, "method");
160
+ const kind = method === null ? void 0 : blocks.get(method.text);
161
+ if (kind === void 0 || kind.moduleOnly && !isModule) {
162
+ return null;
163
+ }
164
+ return field(statement, "block") === null ? null : kind;
165
+ }
166
+ function bodyStatementsRun(body, isModule, blocks) {
167
+ return bodyStatements(body).flatMap((statement) => {
168
+ if (declaredBlockAt(statement, isModule, blocks) === null) {
169
+ return [statement];
170
+ }
171
+ const block = field(statement, "block");
172
+ const inner = block === null ? null : field(block, "body");
173
+ return inner === null ? [] : bodyStatementsRun(inner, isModule, blocks);
174
+ });
175
+ }
176
+ function definedAtClassLevel(node, body, blocks) {
177
+ if (blocks.size === 0) {
178
+ return false;
179
+ }
180
+ const isModule = isModuleBody(body);
181
+ let current = node.parent;
182
+ while (current !== null && current.id !== body.id) {
183
+ const parent = current.parent;
184
+ if (BLOCK_TYPES.has(current.type) && parent !== null) {
185
+ if (declaredBlockAt(parent, isModule, blocks)?.definesClassMethods) {
186
+ return true;
187
+ }
188
+ }
189
+ current = parent;
190
+ }
191
+ return false;
192
+ }
147
193
  function runStatements(body, blockConfigures = () => false) {
148
194
  const found = [];
149
195
  const readsItsChildren = (node) => {
@@ -179,10 +225,10 @@ function stringLiteralValue(node) {
179
225
  function symbolValue(node) {
180
226
  return node.type === "simple_symbol" ? node.text.slice(1) : null;
181
227
  }
182
- function instanceMethodsByName(body) {
228
+ function instanceMethodsByName(body, blocks = NO_BODY_BLOCKS) {
183
229
  const methods = /* @__PURE__ */ new Map();
184
230
  for (const stmt of runStatements(body)) {
185
- if (stmt.type !== "method") {
231
+ if (stmt.type !== "method" || definedAtClassLevel(stmt, body, blocks)) {
186
232
  continue;
187
233
  }
188
234
  const name = field(stmt, "name")?.text;
@@ -244,10 +290,11 @@ function instanceMethodVisibility(body) {
244
290
  }
245
291
  return visibility;
246
292
  }
247
- function singletonMethodsByName(body) {
293
+ function singletonMethodsByName(body, blocks = NO_BODY_BLOCKS) {
248
294
  const methods = /* @__PURE__ */ new Map();
249
295
  for (const stmt of runStatements(body)) {
250
- if (stmt.type !== "singleton_method") {
296
+ const classLevel = stmt.type === "method" && definedAtClassLevel(stmt, body, blocks);
297
+ if (stmt.type !== "singleton_method" && !classLevel) {
251
298
  continue;
252
299
  }
253
300
  const name = field(stmt, "name")?.text;
@@ -360,7 +407,8 @@ function autoloadDirectories(root) {
360
407
  }
361
408
 
362
409
  // src/defineMethod.ts
363
- var import_values4 = require("@suss/values");
410
+ var import_resolution5 = require("@suss/resolution");
411
+ var import_values5 = require("@suss/values");
364
412
 
365
413
  // src/values/evaluator.ts
366
414
  var import_resolution4 = require("@suss/resolution");
@@ -369,35 +417,45 @@ var import_values2 = require("@suss/values");
369
417
  // src/facts/resolve.ts
370
418
  var import_datalog = require("@suss/datalog");
371
419
  var import_resolution = require("@suss/resolution");
372
- var RUBY_RULES = [
373
- // `Loader.new` makes one of the class, which the shared rules already say
374
- // about calling a class. Ruby writes it as a method read off the constant
375
- // instead, so the read is what steps to the class here.
420
+ var RUBY_RULES = (0, import_resolution.alsoSteps)([
421
+ // Making one of a class is a call of the class, which the shared rules
422
+ // already say. Ruby writes the callee as `new` read off the constant, so
423
+ // what this adds is that the callee is the class.
376
424
  (0, import_datalog.rule)(
377
- "stepsTo",
378
- [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("cls"), import_resolution.VALUE_STEP],
425
+ "hop",
426
+ [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("cls"), import_resolution.INSTANCE_STEP],
379
427
  [
380
428
  (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)("new")),
381
429
  (0, import_datalog.lit)("comesTo", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("cls")),
382
430
  (0, import_datalog.lit)("objectValue", (0, import_datalog.variable)("cls"))
383
431
  ]
384
432
  ),
385
- // The shared `declared finder` step, for the same spelling: `Account.first`
386
- // and `Account.where(x).first` write no arguments, so Ruby reads them as
387
- // property reads and neither gets a `call` fact to hang that step on.
433
+ // The same spelling again, for the caller asking where a class was
434
+ // made. `callsNamed` reaches a callee written as the name itself, and
435
+ // Ruby writes `new` off the constant instead.
388
436
  (0, import_datalog.rule)(
389
- "stepsTo",
390
- [(0, import_datalog.variable)("x"), (0, import_datalog.variable)("cls"), import_resolution.VALUE_STEP],
437
+ "constructsNamed",
438
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("cls")],
391
439
  [
392
- (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("x"), (0, import_datalog.variable)("o"), (0, import_datalog.variable)("m")),
393
- (0, import_datalog.lit)("objectOf", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("cls")),
394
- (0, import_datalog.lit)("objectValue", (0, import_datalog.variable)("cls")),
395
- (0, import_datalog.lit)("libraryBase", (0, import_datalog.variable)("cls"), (0, import_datalog.variable)("n")),
396
- (0, import_datalog.lit)("givesBackOne", (0, import_datalog.variable)("n"), (0, import_datalog.variable)("m"))
397
- ],
398
- "declared finder"
440
+ (0, import_datalog.lit)("binds", (0, import_datalog.variable)("o"), (0, import_datalog.variable)("cls")),
441
+ (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)("new")),
442
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))
443
+ ]
444
+ ),
445
+ // `%i[a b].freeze` is worth the list it was written as. The evaluator
446
+ // says the same in its row table, for a value it reads in one file.
447
+ ...["freeze", "dup"].map(
448
+ (method) => (0, import_datalog.rule)(
449
+ "hop",
450
+ [(0, import_datalog.variable)("r"), (0, import_datalog.variable)("o"), import_resolution.VALUE_STEP],
451
+ [
452
+ (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c")),
453
+ (0, import_datalog.lit)("readsProperty", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("o"), (0, import_datalog.constant)(method))
454
+ ],
455
+ "hands back the receiver"
456
+ )
399
457
  )
400
- ];
458
+ ]);
401
459
  var RUBY_PROGRAM = (0, import_resolution.resolutionProgram)(RUBY_RULES);
402
460
  function resolveValues(db, keys) {
403
461
  (0, import_resolution.askResolution)(db, keys, "wanted", RUBY_PROGRAM);
@@ -409,14 +467,122 @@ function writtenValueOf(db, key) {
409
467
  resolveValues(db, [key]);
410
468
  return (0, import_resolution.writtenValueOf)(db, key, (keys) => resolveValues(db, keys));
411
469
  }
470
+ function writtenValueUnder(db, key, site) {
471
+ (0, import_resolution.askResolutionUnder)(db, [[key, site]], (0, import_resolution.resolutionUnderProgram)(RUBY_RULES));
472
+ return (0, import_resolution.writtenValueUnder)(db, key, site);
473
+ }
474
+ function constructionSites(db, classKey) {
475
+ (0, import_resolution.askResolution)(db, [classKey], "wantedSites", RUBY_PROGRAM);
476
+ return (0, import_resolution.allocationSitesOf)(db, classKey);
477
+ }
412
478
 
413
479
  // src/facts/values.ts
414
480
  var import_resolution3 = require("@suss/resolution");
415
481
 
482
+ // src/paths/bareCalls.ts
483
+ var PARAMETER_LIST_TYPES = [
484
+ "method_parameters",
485
+ "block_parameters",
486
+ "lambda_parameters",
487
+ "parameters"
488
+ ];
489
+ function isFieldOf(parent, node, name) {
490
+ return field(parent, name)?.id === node.id;
491
+ }
492
+ function namedAt(parent, node) {
493
+ return isFieldOf(parent, node, "name");
494
+ }
495
+ function always() {
496
+ return true;
497
+ }
498
+ var SPELLS_A_NAME = {
499
+ call: (parent, node) => isFieldOf(parent, node, "method") || isFieldOf(parent, node, "receiver"),
500
+ method: namedAt,
501
+ singleton_method: namedAt,
502
+ assignment: (parent, node) => isFieldOf(parent, node, "left"),
503
+ operator_assignment: (parent, node) => isFieldOf(parent, node, "left"),
504
+ for: (parent, node) => isFieldOf(parent, node, "pattern"),
505
+ optional_parameter: namedAt,
506
+ splat_parameter: namedAt,
507
+ hash_splat_parameter: namedAt,
508
+ keyword_parameter: namedAt,
509
+ block_parameter: namedAt,
510
+ destructured_parameter: always,
511
+ left_assignment_list: always,
512
+ rest_assignment: always,
513
+ destructured_left_assignment: always,
514
+ exception_variable: always,
515
+ undef: always,
516
+ alias: always,
517
+ ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always]))
518
+ };
519
+ function addNamesUnder(node, names) {
520
+ if (node === null) {
521
+ return;
522
+ }
523
+ if (node.type === "identifier") {
524
+ names.add(node.text);
525
+ return;
526
+ }
527
+ for (const child of bodyStatements(node)) {
528
+ addNamesUnder(child, names);
529
+ }
530
+ }
531
+ function parameterNames(node, names) {
532
+ for (const parameter of bodyStatements(node)) {
533
+ if (parameter.type === "identifier") {
534
+ names.add(parameter.text);
535
+ continue;
536
+ }
537
+ const declared = field(parameter, "name");
538
+ if (declared !== null && declared.type === "identifier") {
539
+ names.add(declared.text);
540
+ continue;
541
+ }
542
+ addNamesUnder(parameter, names);
543
+ }
544
+ }
545
+ function assignedNames(node, names) {
546
+ addNamesUnder(field(node, "left"), names);
547
+ }
548
+ var BINDS_A_NAME = {
549
+ assignment: assignedNames,
550
+ operator_assignment: assignedNames,
551
+ for: (node, names) => addNamesUnder(field(node, "pattern"), names),
552
+ exception_variable: (node, names) => addNamesUnder(node, names),
553
+ ...Object.fromEntries(
554
+ PARAMETER_LIST_TYPES.map((type) => [type, parameterNames])
555
+ )
556
+ };
557
+ function localNamesIn(definitionNode) {
558
+ const names = /* @__PURE__ */ new Set();
559
+ const visit = (node) => {
560
+ BINDS_A_NAME[node.type]?.(node, names);
561
+ for (const child of bodyStatements(node)) {
562
+ visit(child);
563
+ }
564
+ };
565
+ visit(definitionNode);
566
+ return names;
567
+ }
568
+ function spellsAName(node) {
569
+ const parent = node.parent;
570
+ if (parent === null) {
571
+ return true;
572
+ }
573
+ return SPELLS_A_NAME[parent.type]?.(parent, node) === true;
574
+ }
575
+ function isBareMethodCall(node, locals) {
576
+ if (node.type !== "identifier" || locals.has(node.text)) {
577
+ return false;
578
+ }
579
+ return !spellsAName(node);
580
+ }
581
+
416
582
  // src/facts/locals.ts
417
583
  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]);
584
+ var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
585
+ var LATER_BODY_TYPES = /* @__PURE__ */ new Set([...OWN_BODY_TYPES, ...BLOCK_TYPES2]);
420
586
  var RUBY_NAME_TYPES = /* @__PURE__ */ new Set([
421
587
  "identifier",
422
588
  "instance_variable"
@@ -437,7 +603,7 @@ function parametersOf(owner) {
437
603
  const params = field(owner, "parameters");
438
604
  return params === null ? [] : bodyStatements(params);
439
605
  }
440
- function parameterNames(owner) {
606
+ function parameterNames2(owner) {
441
607
  const names = /* @__PURE__ */ new Set();
442
608
  for (const param of parametersOf(owner)) {
443
609
  const name = paramNameOf(param);
@@ -485,6 +651,13 @@ function readForVariable(node) {
485
651
  }
486
652
  return targetsOf(pattern).map((target) => writeOf(target, null, node, false));
487
653
  }
654
+ function readRescueVariable(node) {
655
+ const variable = field(node, "variable");
656
+ if (variable === null) {
657
+ return [];
658
+ }
659
+ return bodyStatements(variable).flatMap(targetsOf).map((target) => writeOf(target, null, node, false));
660
+ }
488
661
  function readBlockParameters(node) {
489
662
  const found = [];
490
663
  for (const param of parametersOf(node)) {
@@ -499,6 +672,7 @@ var WRITE_READERS = {
499
672
  assignment: readAssignment,
500
673
  operator_assignment: readOperatorAssignment,
501
674
  for: readForVariable,
675
+ rescue: readRescueVariable,
502
676
  block: readBlockParameters,
503
677
  do_block: readBlockParameters
504
678
  };
@@ -544,7 +718,7 @@ function namesOfMethod(method) {
544
718
  if (remembered !== void 0) {
545
719
  return remembered;
546
720
  }
547
- const names = scopeNames(parameterNames(method), field(method, "body"));
721
+ const names = scopeNames(parameterNames2(method), field(method, "body"));
548
722
  cache.byMethod.set(method, names);
549
723
  return names;
550
724
  }
@@ -566,10 +740,17 @@ function ownerOfName(node, name, enclosing) {
566
740
  }
567
741
  return enclosing !== null && names.locals.has(name) ? enclosing : null;
568
742
  }
743
+ function isLocalName(node, name, enclosing) {
744
+ const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
745
+ if (names.blockParams.has(name) && blockDeclaring(node, name) !== null) {
746
+ return true;
747
+ }
748
+ return names.locals.has(name);
749
+ }
569
750
  function blockDeclaring(node, name) {
570
751
  let current = node.parent;
571
752
  while (current !== null) {
572
- if (BLOCK_TYPES.has(current.type) && parameterNames(current).has(name)) {
753
+ if (BLOCK_TYPES2.has(current.type) && parameterNames2(current).has(name)) {
573
754
  return current;
574
755
  }
575
756
  if (OWN_BODY_TYPES.has(current.type)) {
@@ -625,6 +806,17 @@ function collectWrites(method, body) {
625
806
  }
626
807
  return [...groups.values()];
627
808
  }
809
+ function instanceWritesRunInOrder(body, name, targets) {
810
+ return (0, import_resolution2.writesRunInOrder)(
811
+ body,
812
+ name,
813
+ targets.map((target) => ({
814
+ at: target,
815
+ direct: target.parent?.parent?.id === body.id
816
+ })),
817
+ nameReads(new Set(targets.map((target) => target.id)))
818
+ );
819
+ }
628
820
  function orderedWrite(write, scope) {
629
821
  return {
630
822
  at: write.at,
@@ -670,12 +862,28 @@ function readKey(filePath, written, enclosing) {
670
862
  if (node.type !== "identifier" && node.type !== "constant") {
671
863
  return nodeId(filePath, node);
672
864
  }
673
- const parent = node.parent;
674
- if (parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null) {
675
- return isPropertyRead(parent) ? nodeId(filePath, parent) : nodeId(filePath, node);
865
+ if (isMethodOfReceiver(node) || isBareCall(node, enclosing)) {
866
+ return nodeId(filePath, node);
676
867
  }
677
868
  return nameKey(filePath, node, enclosing);
678
869
  }
870
+ function isMethodOfReceiver(node) {
871
+ const parent = node.parent;
872
+ return parent !== null && parent.type === "call" && field(parent, "method")?.id === node.id && field(parent, "receiver") !== null;
873
+ }
874
+ function isReceiverOfCall(node) {
875
+ const parent = node.parent;
876
+ return parent !== null && parent.type === "call" && field(parent, "receiver")?.id === node.id;
877
+ }
878
+ function isBareCall(node, enclosing) {
879
+ if (node.type !== "identifier") {
880
+ return false;
881
+ }
882
+ if (spellsAName(node) && !isReceiverOfCall(node)) {
883
+ return false;
884
+ }
885
+ return !isLocalName(node, node.text, enclosing);
886
+ }
679
887
  var WRITTEN_VALUE_TYPES = /* @__PURE__ */ new Set([
680
888
  "string",
681
889
  "integer",
@@ -704,11 +912,7 @@ function add(emitter, relation, ...tuple) {
704
912
  emitter.db.add(relation, tuple);
705
913
  }
706
914
  function valueKey(emitter, written) {
707
- const value = readThrough(written);
708
- if (value.type !== "identifier" && value.type !== "constant") {
709
- return nodeId(emitter.filePath, value);
710
- }
711
- return nameKey(emitter.filePath, value, emitter.enclosing);
915
+ return readKey(emitter.filePath, written, emitter.enclosing);
712
916
  }
713
917
  function pairKeyText(key) {
714
918
  if (key.type === "hash_key_symbol") {
@@ -722,35 +926,29 @@ function pairKeyText(key) {
722
926
  }
723
927
  return null;
724
928
  }
725
- function isPropertyRead(node) {
726
- return node.type === "call" && field(node, "receiver") !== null && field(node, "arguments") === null && children(node).every(
727
- (child) => child.type !== "do_block" && child.type !== "block"
728
- );
729
- }
730
- function emitPropertyRead(emitter, node) {
731
- const receiver = field(node, "receiver");
732
- const method = field(node, "method");
733
- if (receiver === null || method === null) {
734
- return;
929
+ function calleeKeyOf(filePath, node, enclosing) {
930
+ if (isBareCall(node, enclosing)) {
931
+ return nameKey(filePath, node, enclosing);
735
932
  }
736
- add(
737
- emitter,
738
- "readsProperty",
739
- nodeId(emitter.filePath, node),
740
- valueKey(emitter, receiver),
741
- method.text
742
- );
933
+ const method = node.type === "call" ? field(node, "method") : null;
934
+ if (method === null) {
935
+ return null;
936
+ }
937
+ return field(node, "receiver") === null ? nameKey(filePath, method, enclosing) : nodeId(filePath, method);
743
938
  }
744
939
  function emitCall(emitter, call) {
745
940
  add(emitter, "writtenValue", nodeId(emitter.filePath, call));
746
- const method = field(call, "method");
747
- if (method === null) {
941
+ const method = call.type === "identifier" ? call : field(call, "method");
942
+ const calleeKey = calleeKeyOf(emitter.filePath, call, emitter.enclosing);
943
+ if (method === null || calleeKey === null) {
748
944
  return;
749
945
  }
750
946
  const receiver = field(call, "receiver");
751
947
  const callKey = nodeId(emitter.filePath, call);
752
- const calleeKey = receiver === null ? valueKey(emitter, method) : nodeId(emitter.filePath, method);
753
948
  add(emitter, "call", callKey, calleeKey);
949
+ if (!emitter.insideMethod) {
950
+ add(emitter, "callOutsideMethod", callKey);
951
+ }
754
952
  if (receiver !== null) {
755
953
  add(
756
954
  emitter,
@@ -759,6 +957,8 @@ function emitCall(emitter, call) {
759
957
  valueKey(emitter, receiver),
760
958
  method.text
761
959
  );
960
+ } else if (emitter.selfKey !== null) {
961
+ add(emitter, "readsProperty", calleeKey, emitter.selfKey, method.text);
762
962
  }
763
963
  const args = field(call, "arguments");
764
964
  let position = 0;
@@ -819,20 +1019,71 @@ function emitHash(emitter, hash) {
819
1019
  add(emitter, "holdsProperty", objectKey, keyText, valueKey(emitter, value));
820
1020
  }
821
1021
  }
822
- function walkExpressions(node, visit) {
1022
+ var BLOCK_TYPES3 = /* @__PURE__ */ new Set(["block", "do_block"]);
1023
+ var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index", "map"]);
1024
+ var DEFINE_METHOD_CALL = "define_method";
1025
+ function loopTurnAt(emitter, block) {
1026
+ const call = block.parent;
1027
+ if (call?.type !== "call") {
1028
+ return null;
1029
+ }
1030
+ const receiver = field(call, "receiver");
1031
+ const method = field(call, "method");
1032
+ if (receiver === null || method === null || !LOOP_METHODS.has(method.text)) {
1033
+ return null;
1034
+ }
1035
+ const parameters = field(block, "parameters");
1036
+ const names = parameters === null ? [] : children(parameters).map((p) => p.text);
1037
+ const element = names[0];
1038
+ if (element === void 0) {
1039
+ return null;
1040
+ }
1041
+ return {
1042
+ element,
1043
+ index: names[1] ?? "",
1044
+ overKey: nodeId(emitter.filePath, receiver)
1045
+ };
1046
+ }
1047
+ function emitDynamicDefinition(emitter, call, turns) {
1048
+ if (emitter.selfKey === null || // A call inside a method runs when that method does, not at load time.
1049
+ emitter.enclosing !== null || call.type !== "call" || field(call, "receiver") !== null || field(call, "method")?.text !== DEFINE_METHOD_CALL) {
1050
+ return;
1051
+ }
1052
+ const args = field(call, "arguments");
1053
+ const first = args === null ? void 0 : children(args)[0];
1054
+ const nameKey2 = nodeId(emitter.filePath, first ?? call);
1055
+ add(emitter, "definesMethodFrom", emitter.selfKey, nameKey2);
1056
+ for (const turn of turns) {
1057
+ add(
1058
+ emitter,
1059
+ "nameTurnsOn",
1060
+ nameKey2,
1061
+ turn.element,
1062
+ turn.index,
1063
+ turn.overKey
1064
+ );
1065
+ }
1066
+ }
1067
+ function walkExpressions(node, emitter, visit, turns = []) {
823
1068
  for (const child of children(node)) {
824
1069
  if (OWN_BODY_TYPES.has(child.type)) {
825
1070
  continue;
826
1071
  }
827
- visit(child);
828
- walkExpressions(child, visit);
1072
+ visit(child, turns);
1073
+ const opened = BLOCK_TYPES3.has(child.type) ? loopTurnAt(emitter, child) : null;
1074
+ walkExpressions(
1075
+ child,
1076
+ emitter,
1077
+ visit,
1078
+ opened === null ? turns : [...turns, opened]
1079
+ );
829
1080
  }
830
1081
  }
831
1082
  function emitExpressionFacts(emitter, node) {
832
- walkExpressions(node, (child) => {
833
- if (isPropertyRead(child)) {
834
- emitPropertyRead(emitter, child);
835
- } else if (child.type === "call") {
1083
+ emitDynamicDefinition(emitter, node, []);
1084
+ walkExpressions(node, emitter, (child, turns) => {
1085
+ emitDynamicDefinition(emitter, child, turns);
1086
+ if (child.type === "call" || isBareCall(child, emitter.enclosing)) {
836
1087
  emitCall(emitter, child);
837
1088
  }
838
1089
  if (ARRAY_TYPES.has(child.type)) {
@@ -888,28 +1139,52 @@ function collectInstanceWrite(emitter, node) {
888
1139
  const operator = field(node, "operator")?.text;
889
1140
  const value = node.type === "assignment" || WHOLE_VALUE_OPERATORS.has(operator ?? "") ? right : null;
890
1141
  const written = collector.get(left.text) ?? [];
891
- written.push(
892
- describeWrite(emitter, {
1142
+ written.push({
1143
+ write: describeWrite(emitter, {
893
1144
  name: left.text,
894
1145
  target: left,
895
1146
  value,
896
1147
  at: node,
897
1148
  fromParameter: false
898
- })
899
- );
1149
+ }),
1150
+ target: left
1151
+ });
900
1152
  collector.set(left.text, written);
901
1153
  }
1154
+ function settledWrites(writes, ordered) {
1155
+ const described = writes.map((written) => written.write);
1156
+ const settled = (0, import_resolution3.valueLeftByWrites)(described, ordered);
1157
+ if (settled !== null) {
1158
+ return [settled];
1159
+ }
1160
+ const left = [];
1161
+ for (const write of described) {
1162
+ if (write.value !== null && !write.narrowsName) {
1163
+ left.push(write.value);
1164
+ }
1165
+ }
1166
+ return left;
1167
+ }
902
1168
  function emitInstanceWrites(emitter, classKey, collected) {
903
1169
  for (const [name, writes] of collected) {
904
- const settled = (0, import_resolution3.valueLeftByWrites)(writes, false);
905
- if (settled !== null) {
906
- add(emitter, "holdsProperty", classKey, name, settled);
1170
+ for (const value of settledWrites(writes, false)) {
1171
+ add(emitter, "holdsProperty", classKey, name, value);
1172
+ }
1173
+ }
1174
+ }
1175
+ function emitInstanceStores(emitter, funcKey, body, collected) {
1176
+ for (const [name, writes] of collected) {
1177
+ const deciding = writes.filter((written) => !written.write.narrowsName);
1178
+ if (deciding.length === 0) {
907
1179
  continue;
908
1180
  }
909
- for (const write of writes) {
910
- if (write.value !== null && !write.narrowsName) {
911
- add(emitter, "holdsProperty", classKey, name, write.value);
912
- }
1181
+ const ordered = instanceWritesRunInOrder(
1182
+ body,
1183
+ name,
1184
+ deciding.map((written) => written.target)
1185
+ );
1186
+ for (const value of settledWrites(deciding, ordered)) {
1187
+ add(emitter, "storesProperty", funcKey, name, value);
913
1188
  }
914
1189
  }
915
1190
  }
@@ -918,7 +1193,20 @@ function implicitReturn(body) {
918
1193
  (child) => child.type !== "rescue" && child.type !== "ensure"
919
1194
  );
920
1195
  const last = statements[statements.length - 1];
921
- return last === void 0 || last.type === "return" ? null : last;
1196
+ if (last === void 0 || last.type === "return") {
1197
+ return null;
1198
+ }
1199
+ return assignedValueOf(last) ?? last;
1200
+ }
1201
+ function assignedValueOf(node) {
1202
+ if (!ASSIGNMENT_TYPES.has(node.type)) {
1203
+ return null;
1204
+ }
1205
+ const operator = field(node, "operator")?.text;
1206
+ if (node.type !== "assignment" && !WHOLE_VALUE_OPERATORS.has(operator ?? "")) {
1207
+ return null;
1208
+ }
1209
+ return field(node, "right");
922
1210
  }
923
1211
  function emitMethodFacts(emitter, method) {
924
1212
  const funcKey = nodeId(emitter.filePath, method);
@@ -948,7 +1236,7 @@ function emitMethodFacts(emitter, method) {
948
1236
  }
949
1237
  };
950
1238
  recordNested(body);
951
- walkExpressions(body, (child) => {
1239
+ walkExpressions(body, inside, (child) => {
952
1240
  if (child.type === "return") {
953
1241
  const first = children(child)[0];
954
1242
  const returned = first?.type === "argument_list" ? children(first)[0] : first;
@@ -956,7 +1244,7 @@ function emitMethodFacts(emitter, method) {
956
1244
  add(inside, "returnsValue", funcKey, valueKey(inside, returned));
957
1245
  }
958
1246
  }
959
- if (child.type === "call") {
1247
+ if (child.type === "call" || isBareCall(child, method)) {
960
1248
  add(inside, "bodyCalls", funcKey, nodeId(inside.filePath, child));
961
1249
  }
962
1250
  });
@@ -1024,37 +1312,64 @@ function emitScopeWrites(emitter, method, body) {
1024
1312
  }
1025
1313
  }
1026
1314
  function emitCandidates(emitter, key, group) {
1315
+ let unstated = false;
1316
+ let stated = 0;
1027
1317
  for (const write of group.writes) {
1028
1318
  if (write.value === null) {
1029
1319
  add(emitter, "writesUnstated", key);
1320
+ unstated = true;
1030
1321
  continue;
1031
1322
  }
1032
1323
  if (write.fromParameter) {
1324
+ unstated = true;
1033
1325
  continue;
1034
1326
  }
1035
1327
  if (!describeWrite(emitter, write).narrowsName) {
1036
1328
  add(emitter, "mayHold", key, valueKey(emitter, write.value));
1329
+ stated += 1;
1037
1330
  }
1038
1331
  }
1332
+ if (!unstated && stated > 0) {
1333
+ add(emitter, "writesAllStated", key);
1334
+ }
1039
1335
  }
1040
1336
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1337
+ var INITIALIZE_METHOD = "initialize";
1338
+ function receiverKeyOf(filePath, method, classKey) {
1339
+ return method.type === "singleton_method" ? classKey : `${nodeId(filePath, method)}#self`;
1340
+ }
1341
+ var CONSTANT_REF_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
1342
+ function mixedInConstants(body, callName) {
1343
+ return bareCallArgumentGroups(body, callName).flatMap((group) => [...group].reverse()).reverse().map(readThrough).filter((argument) => CONSTANT_REF_TYPES.has(argument.type));
1344
+ }
1345
+ function emitMixinFacts(emitter, classKey, body, callName) {
1346
+ for (const mixin of mixedInConstants(body, callName)) {
1347
+ add(emitter, "extends", classKey, valueKey(emitter, mixin));
1348
+ }
1349
+ }
1041
1350
  function emitClassFacts(emitter, cls) {
1042
1351
  const classKey = nodeId(emitter.filePath, cls);
1043
1352
  add(emitter, "objectValue", classKey);
1353
+ const body = field(cls, "body");
1354
+ if (body !== null) {
1355
+ emitMixinFacts(emitter, classKey, body, PREPEND_CALL);
1356
+ emitMixinFacts(emitter, classKey, body, INCLUDE_CALL);
1357
+ }
1044
1358
  const superclass = field(cls, "superclass");
1045
1359
  const base = superclass === null ? null : children(superclass)[0] ?? null;
1046
1360
  if (base !== null) {
1047
1361
  add(emitter, "extends", classKey, valueKey(emitter, base));
1048
1362
  add(emitter, "extendsNamed", classKey, base.text);
1049
1363
  }
1050
- const body = field(cls, "body");
1051
1364
  const collected = /* @__PURE__ */ new Map();
1052
1365
  const within = {
1053
1366
  ...emitter,
1054
1367
  selfKey: classKey,
1368
+ insideMethod: false,
1055
1369
  instanceWrites: collected
1056
1370
  };
1057
- for (const statement of body === null ? [] : children(body)) {
1371
+ const statements = body === null ? [] : bodyStatementsRun(body, cls.type === "module", emitter.bodyBlocks);
1372
+ for (const statement of statements) {
1058
1373
  if (statement.type === "assignment") {
1059
1374
  const left = field(statement, "left");
1060
1375
  const right = field(statement, "right");
@@ -1075,24 +1390,47 @@ function emitClassFacts(emitter, cls) {
1075
1390
  continue;
1076
1391
  }
1077
1392
  if (!METHOD_TYPES.has(statement.type)) {
1393
+ emitExpressionFacts(within, statement);
1078
1394
  continue;
1079
1395
  }
1080
- const funcKey = emitMethodFacts(within, statement);
1396
+ const stored = /* @__PURE__ */ new Map();
1397
+ const receiverKey = receiverKeyOf(emitter.filePath, statement, classKey);
1398
+ if (receiverKey !== classKey) {
1399
+ add(emitter, "instanceOf", receiverKey, classKey);
1400
+ }
1401
+ const funcKey = emitMethodFacts(
1402
+ {
1403
+ ...within,
1404
+ selfKey: receiverKey,
1405
+ insideMethod: receiverKey !== classKey,
1406
+ instanceWrites: stored
1407
+ },
1408
+ statement
1409
+ );
1081
1410
  const name = field(statement, "name");
1082
1411
  if (name !== null) {
1083
1412
  add(emitter, "holdsProperty", classKey, name.text, funcKey);
1413
+ if (name.text === INITIALIZE_METHOD) {
1414
+ add(emitter, "initializes", classKey, funcKey);
1415
+ }
1416
+ }
1417
+ const methodBody = field(statement, "body");
1418
+ if (methodBody !== null) {
1419
+ emitInstanceStores(within, funcKey, methodBody, stored);
1084
1420
  }
1085
1421
  }
1086
1422
  emitInstanceWrites(within, classKey, collected);
1087
1423
  return classKey;
1088
1424
  }
1089
- function emitValueFacts(db, filePath, root) {
1425
+ function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
1090
1426
  const emitter = {
1091
1427
  db,
1092
1428
  filePath,
1093
1429
  enclosing: null,
1094
1430
  selfKey: null,
1095
- instanceWrites: null
1431
+ insideMethod: false,
1432
+ instanceWrites: null,
1433
+ bodyBlocks
1096
1434
  };
1097
1435
  const declaresName = (child, key) => {
1098
1436
  const name = field(child, "name");
@@ -1132,7 +1470,7 @@ var OWN_ROOT_TYPES = /* @__PURE__ */ new Set([
1132
1470
  "module",
1133
1471
  "singleton_class"
1134
1472
  ]);
1135
- var BLOCK_TYPES2 = /* @__PURE__ */ new Set(["block", "do_block"]);
1473
+ var BLOCK_TYPES4 = /* @__PURE__ */ new Set(["block", "do_block"]);
1136
1474
  var METHOD_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1137
1475
  var NAME_TYPES = /* @__PURE__ */ new Set([
1138
1476
  "identifier",
@@ -1171,7 +1509,7 @@ function rubyLowering(options) {
1171
1509
  statement: statementOf,
1172
1510
  siteOf,
1173
1511
  functionOf,
1174
- writtenTo: (node) => context === null ? null : context.writtenTo(node),
1512
+ writtenTo: (node, site) => context === null ? null : context.writtenTo(node, site),
1175
1513
  callable: (node) => {
1176
1514
  if (!isInlinableCall(node)) {
1177
1515
  return null;
@@ -1641,7 +1979,7 @@ function isTailExpression(node) {
1641
1979
  if (owner.type === "block" && owner.parent?.type === "lambda") {
1642
1980
  return true;
1643
1981
  }
1644
- if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES2.has(owner.type)) {
1982
+ if (METHOD_TYPES2.has(owner.type) || BLOCK_TYPES4.has(owner.type)) {
1645
1983
  return true;
1646
1984
  }
1647
1985
  if (owner.type === "when") {
@@ -1660,7 +1998,7 @@ function isRoot(node) {
1660
1998
  if (OWN_ROOT_TYPES.has(node.type)) {
1661
1999
  return true;
1662
2000
  }
1663
- return BLOCK_TYPES2.has(node.type) && node.parent?.type === "call";
2001
+ return BLOCK_TYPES4.has(node.type) && node.parent?.type === "call";
1664
2002
  }
1665
2003
  function isPathStatement(node, parent) {
1666
2004
  if (node.type === "elsif") {
@@ -1688,17 +2026,32 @@ function siteOf(node) {
1688
2026
  current = parent;
1689
2027
  }
1690
2028
  }
2029
+ var BARE_SPLAT_TYPES = /* @__PURE__ */ new Set(["splat_parameter", "hash_splat_parameter"]);
1691
2030
  function parametersOf2(parameters) {
1692
2031
  if (parameters === null) {
1693
2032
  return [];
1694
2033
  }
1695
- return named(parameters).flatMap((parameter) => {
2034
+ const declared = [];
2035
+ let position = 0;
2036
+ for (const parameter of named(parameters)) {
2037
+ if (BARE_SPLAT_TYPES.has(parameter.type) && field(parameter, "name") === null) {
2038
+ continue;
2039
+ }
1696
2040
  if (parameter.type === "identifier") {
1697
- return [{ name: parameter.text, default: null }];
2041
+ declared.push({ name: parameter.text, default: null, position });
2042
+ } else {
2043
+ const name = field(parameter, "name");
2044
+ if (name !== null) {
2045
+ declared.push({
2046
+ name: name.text,
2047
+ default: field(parameter, "value"),
2048
+ position
2049
+ });
2050
+ }
1698
2051
  }
1699
- const name = field(parameter, "name");
1700
- return name === null ? [] : [{ name: name.text, default: field(parameter, "value") }];
1701
- });
2052
+ position += 1;
2053
+ }
2054
+ return declared;
1702
2055
  }
1703
2056
  function functionOf(node) {
1704
2057
  if (node.type === "program") {
@@ -1714,7 +2067,7 @@ function functionOf(node) {
1714
2067
  body: bodyOf(block)
1715
2068
  };
1716
2069
  }
1717
- if (BLOCK_TYPES2.has(node.type)) {
2070
+ if (BLOCK_TYPES4.has(node.type)) {
1718
2071
  return {
1719
2072
  parameters: parametersOf2(field(node, "parameters")),
1720
2073
  body: bodyOf(node)
@@ -2088,25 +2441,56 @@ var rubyRows = [
2088
2441
 
2089
2442
  // src/values/evaluator.ts
2090
2443
  var evaluators = /* @__PURE__ */ new WeakMap();
2444
+ var contexts = /* @__PURE__ */ new WeakMap();
2091
2445
  var withoutFacts = /* @__PURE__ */ new WeakMap();
2446
+ var filesByTree = /* @__PURE__ */ new WeakMap();
2092
2447
  var METHOD_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
2093
2448
  function bindEvaluator(db, nodes) {
2094
- evaluators.set(
2095
- db,
2096
- new import_values2.Evaluator(
2097
- rubyLowering({ context: contextOver(db, nodes), rows: rubyRows })
2098
- )
2099
- );
2449
+ for (const entry of nodes.files) {
2450
+ filesByTree.set(entry.root.tree, entry.file);
2451
+ }
2452
+ const context = contextOver(db, nodes);
2453
+ contexts.set(db, context);
2454
+ evaluators.set(db, new import_values2.Evaluator(rubyLowering({ context, rows: rubyRows })));
2455
+ }
2456
+ function writtenNodeOf(node, db, site) {
2457
+ const context = db === void 0 ? void 0 : contexts.get(db);
2458
+ return context === void 0 ? null : context.writtenTo(node, site);
2100
2459
  }
2101
- function evaluatedValue(node, db, bindings) {
2460
+ function evaluatedValue(node, db, bindings, site) {
2102
2461
  const evaluator = evaluatorFor(node, db);
2103
2462
  if (bindings === void 0) {
2104
- return (0, import_values2.force)(evaluator.evaluate(node));
2463
+ return (0, import_values2.force)(
2464
+ site === void 0 ? evaluator.evaluate(node) : evaluator.evaluate(node, { site })
2465
+ );
2105
2466
  }
2106
2467
  const supplied = new Map(
2107
2468
  [...bindings].map(([name, value]) => [name, (0, import_values2.text)(value)])
2108
2469
  );
2109
- return (0, import_values2.force)(evaluator.evaluate(node, { bindings: supplied }));
2470
+ return (0, import_values2.force)(
2471
+ evaluator.evaluate(node, {
2472
+ bindings: supplied,
2473
+ ...site === void 0 ? {} : { site }
2474
+ })
2475
+ );
2476
+ }
2477
+ function constructionSitesOf(node, db) {
2478
+ const cls = enclosingClass(node);
2479
+ const file = cls === null ? void 0 : filesByTree.get(cls.tree);
2480
+ if (db === void 0 || cls === null || file === void 0) {
2481
+ return [];
2482
+ }
2483
+ return constructionSites(db, nodeId(file, cls));
2484
+ }
2485
+ function enclosingClass(node) {
2486
+ let current = node.parent;
2487
+ while (current !== null) {
2488
+ if (current.type === "class") {
2489
+ return current;
2490
+ }
2491
+ current = current.parent;
2492
+ }
2493
+ return null;
2110
2494
  }
2111
2495
  function stringValueOf(node, db, bindings) {
2112
2496
  return (0, import_values2.literalOf)(evaluatedValue(node, db, bindings));
@@ -2158,15 +2542,13 @@ function contextOver(db, nodes) {
2158
2542
  }
2159
2543
  const fileOf = (node) => filesByRoot.get(node.tree.rootNode.id)?.file ?? null;
2160
2544
  return {
2161
- writtenTo: (node) => {
2545
+ writtenTo: (node, site) => {
2162
2546
  const file = fileOf(node);
2163
2547
  if (file === null) {
2164
2548
  return null;
2165
2549
  }
2166
- const answer = writtenValueOf(
2167
- db,
2168
- readKey(file, node, enclosingMethod(node))
2169
- );
2550
+ const key = readKey(file, node, enclosingMethod(node));
2551
+ const answer = site === void 0 ? writtenValueOf(db, key) : writtenValueUnder(db, key, site);
2170
2552
  return answer === null ? null : (0, import_resolution4.nodeOfKey)(rootsByFile, answer);
2171
2553
  },
2172
2554
  callable: (call) => {
@@ -2184,125 +2566,141 @@ function contextOver(db, nodes) {
2184
2566
  };
2185
2567
  }
2186
2568
 
2187
- // 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;
2569
+ // src/values/literals.ts
2570
+ var import_values4 = require("@suss/values");
2571
+ function literalElementsIn(value) {
2572
+ if (value.kind !== "sequence") {
2573
+ return null;
2211
2574
  }
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;
2575
+ const elements = [];
2576
+ for (const item of value.items) {
2577
+ const literal = (0, import_values4.literalOf)((0, import_values4.force)(item.value));
2578
+ if (literal === null) {
2579
+ return null;
2219
2580
  }
2220
- for (const name of defined) {
2221
- names.add(name);
2581
+ elements.push(literal);
2582
+ }
2583
+ return elements;
2584
+ }
2585
+ function literalElementsOf(node, db) {
2586
+ return literalElementsIn(evaluatedValue(node, db));
2587
+ }
2588
+ function namesOf(node, db) {
2589
+ const value = evaluatedValue(node, db);
2590
+ const single = (0, import_values4.literalOf)(value);
2591
+ return single === null ? literalElementsIn(value) : [single];
2592
+ }
2593
+
2594
+ // src/defineMethod.ts
2595
+ var NOTHING = {
2596
+ names: /* @__PURE__ */ new Set(),
2597
+ patterns: [],
2598
+ unreadable: false
2599
+ };
2600
+ function couldBeDefined(defined, name) {
2601
+ return defined.unreadable || defined.patterns.some((pattern) => pattern.test(name));
2602
+ }
2603
+ function definedNamesOf(dynamic, classKey) {
2604
+ return dynamic?.get(classKey) ?? NOTHING;
2605
+ }
2606
+ function readDynamicNames(db, rootsByFile) {
2607
+ const byClass = /* @__PURE__ */ new Map();
2608
+ for (const [classKey, nameKey2] of db.facts("definesMethodFrom")) {
2609
+ const key = String(classKey);
2610
+ byClass.set(
2611
+ key,
2612
+ foldReading(
2613
+ byClass.get(key) ?? NOTHING,
2614
+ readOne(db, rootsByFile, String(nameKey2))
2615
+ )
2616
+ );
2617
+ }
2618
+ for (const [classKey, reading] of byClass) {
2619
+ for (const name of reading.names) {
2620
+ db.add("declaresName", [classKey, name]);
2222
2621
  }
2223
2622
  }
2224
- return { names, unreadable };
2623
+ return byClass;
2225
2624
  }
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;
2625
+ function foldReading(soFar, reading) {
2626
+ if (reading === null) {
2627
+ return { ...soFar, unreadable: true };
2231
2628
  }
2629
+ return {
2630
+ names: /* @__PURE__ */ new Set([...soFar.names, ...reading.names]),
2631
+ patterns: [...soFar.patterns, ...reading.patterns],
2632
+ unreadable: soFar.unreadable
2633
+ };
2634
+ }
2635
+ function readOne(db, rootsByFile, nameKey2) {
2636
+ const node = (0, import_resolution5.nodeOfKey)(rootsByFile, nameKey2);
2232
2637
  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) {
2638
+ const patterns = [];
2639
+ for (const bindings of loopTurns(db, rootsByFile, nameKey2)) {
2640
+ const value = node === null ? (0, import_values5.hole)(nameKey2) : evaluatedValue(node, db, bindings.size === 0 ? void 0 : bindings);
2641
+ const name = (0, import_values5.literalOf)(value);
2642
+ if (name !== null) {
2643
+ names.push(name);
2644
+ continue;
2645
+ }
2646
+ const pattern = patternOf(value);
2647
+ if (pattern === null) {
2240
2648
  return null;
2241
2649
  }
2242
- names.push(name);
2650
+ patterns.push(pattern);
2243
2651
  }
2244
- return names;
2652
+ return { names, patterns };
2245
2653
  }
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) {
2654
+ function patternOf(value) {
2655
+ const pieces = (0, import_values5.piecesOf)(value);
2656
+ let source = "^";
2657
+ let readSomething = false;
2658
+ for (const piece of pieces) {
2659
+ if (piece.kind === "hole") {
2660
+ source += "[\\s\\S]*";
2251
2661
  continue;
2252
2662
  }
2253
- turns = turns.flatMap(
2254
- (base) => perElement.map((element) => new Map([...base, ...element]))
2255
- );
2256
- }
2257
- return turns;
2258
- }
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 });
2663
+ const options = piece.options.filter((option) => option.length > 0);
2664
+ if (options.length === 0) {
2665
+ continue;
2266
2666
  }
2267
- current = parent;
2667
+ readSomething = true;
2668
+ source += `(?:${options.map(escapeForPattern).join("|")})`;
2268
2669
  }
2269
- return blocks;
2670
+ return readSomething ? new RegExp(`${source}$`) : null;
2270
2671
  }
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
- });
2672
+ function escapeForPattern(text2) {
2673
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2291
2674
  }
2292
- function literalElementsOf(node) {
2293
- const value = evaluatedValue(node);
2294
- if (value.kind !== "sequence") {
2295
- return null;
2675
+ function loopTurns(db, rootsByFile, nameKey2) {
2676
+ let turns = [/* @__PURE__ */ new Map()];
2677
+ for (const over of listsBehind(db, rootsByFile, nameKey2)) {
2678
+ turns = turns.flatMap(
2679
+ (base) => over.elements.map((value, position) => {
2680
+ const binding = new Map([...base, [over.element, value]]);
2681
+ if (over.index !== "") {
2682
+ binding.set(over.index, String(position));
2683
+ }
2684
+ return binding;
2685
+ })
2686
+ );
2296
2687
  }
2297
- const elements = [];
2298
- for (const item of value.items) {
2299
- const literal = (0, import_values4.literalOf)((0, import_values4.force)(item.value));
2300
- if (literal === null) {
2301
- return null;
2688
+ return turns;
2689
+ }
2690
+ function listsBehind(db, rootsByFile, nameKey2) {
2691
+ const found = [];
2692
+ for (const [, element, index, overKey] of db.lookup(
2693
+ "nameTurnsOn",
2694
+ 0,
2695
+ nameKey2
2696
+ )) {
2697
+ const node = (0, import_resolution5.nodeOfKey)(rootsByFile, String(overKey));
2698
+ const elements = node === null ? null : literalElementsOf(node, db);
2699
+ if (elements !== null) {
2700
+ found.push({ element: String(element), index: String(index), elements });
2302
2701
  }
2303
- elements.push(literal);
2304
2702
  }
2305
- return elements;
2703
+ return found;
2306
2704
  }
2307
2705
 
2308
2706
  // src/scope.ts
@@ -2431,8 +2829,6 @@ function graphqlTypeNameFromQualified(qualifiedName, convention) {
2431
2829
  }
2432
2830
 
2433
2831
  // src/ancestry.ts
2434
- var INCLUDE_CALL = "include";
2435
- var PREPEND_CALL = "prepend";
2436
2832
  async function reachDefinition(qualifiedName, lookup) {
2437
2833
  const filePath = resolveConstantFile(
2438
2834
  lookup.root,
@@ -2559,7 +2955,7 @@ function superclassCandidatesOf(blocks) {
2559
2955
  }
2560
2956
  return [];
2561
2957
  }
2562
- function methodInAncestry(ancestry, name) {
2958
+ function methodInAncestry(ancestry, name, read = {}) {
2563
2959
  for (const entry of ancestry) {
2564
2960
  if (entry.type === "root") {
2565
2961
  return { type: "none" };
@@ -2571,7 +2967,7 @@ function methodInAncestry(ancestry, name) {
2571
2967
  cause: "unreadAncestor"
2572
2968
  };
2573
2969
  }
2574
- const found = definitionIn(entry.blocks, name);
2970
+ const found = definitionIn(entry.blocks, name, read);
2575
2971
  if (found.method !== null && found.block !== null) {
2576
2972
  return { type: "found", method: found.method, block: found.block };
2577
2973
  }
@@ -2585,7 +2981,7 @@ function methodInAncestry(ancestry, name) {
2585
2981
  }
2586
2982
  return { type: "none" };
2587
2983
  }
2588
- function definitionIn(blocks, name) {
2984
+ function definitionIn(blocks, name, read) {
2589
2985
  let method = null;
2590
2986
  let block = null;
2591
2987
  let definedDynamically = false;
@@ -2595,14 +2991,17 @@ function definitionIn(blocks, name) {
2595
2991
  if (body === null) {
2596
2992
  continue;
2597
2993
  }
2598
- const found = instanceMethodsByName(body).get(name);
2994
+ const found = instanceMethodsByName(body, read.bodyBlocks).get(name);
2599
2995
  if (found !== void 0) {
2600
2996
  method = found;
2601
2997
  block = candidate;
2602
2998
  }
2603
- const defined = defineMethodNames(body);
2999
+ const defined = definedNamesOf(
3000
+ read.dynamicNames,
3001
+ nodeId(candidate.file, candidate.info.node)
3002
+ );
2604
3003
  definedDynamically ||= defined.names.has(name);
2605
- unreadableDefine ||= defined.unreadable;
3004
+ unreadableDefine ||= couldBeDefined(defined, name);
2606
3005
  }
2607
3006
  return { method, block, definedDynamically, unreadableDefine };
2608
3007
  }
@@ -2631,107 +3030,11 @@ var import_extractor5 = require("@suss/extractor");
2631
3030
 
2632
3031
  // src/clientCalls.ts
2633
3032
  var import_behavioral_ir = require("@suss/behavioral-ir");
2634
- var import_values6 = require("@suss/values");
3033
+ var import_values9 = require("@suss/values");
2635
3034
 
2636
3035
  // src/paths/effects.ts
2637
3036
  var import_extractor2 = require("@suss/extractor");
2638
-
2639
- // src/paths/bareCalls.ts
2640
- var PARAMETER_LIST_TYPES = [
2641
- "method_parameters",
2642
- "block_parameters",
2643
- "lambda_parameters",
2644
- "parameters"
2645
- ];
2646
- function isFieldOf(parent, node, name) {
2647
- return field(parent, name)?.id === node.id;
2648
- }
2649
- function namedAt(parent, node) {
2650
- return isFieldOf(parent, node, "name");
2651
- }
2652
- function always() {
2653
- return true;
2654
- }
2655
- var SPELLS_A_NAME = {
2656
- call: (parent, node) => isFieldOf(parent, node, "method") || isFieldOf(parent, node, "receiver"),
2657
- method: namedAt,
2658
- singleton_method: namedAt,
2659
- assignment: (parent, node) => isFieldOf(parent, node, "left"),
2660
- operator_assignment: (parent, node) => isFieldOf(parent, node, "left"),
2661
- for: (parent, node) => isFieldOf(parent, node, "pattern"),
2662
- optional_parameter: namedAt,
2663
- splat_parameter: namedAt,
2664
- hash_splat_parameter: namedAt,
2665
- keyword_parameter: namedAt,
2666
- block_parameter: namedAt,
2667
- destructured_parameter: always,
2668
- left_assignment_list: always,
2669
- rest_assignment: always,
2670
- destructured_left_assignment: always,
2671
- exception_variable: always,
2672
- undef: always,
2673
- alias: always,
2674
- ...Object.fromEntries(PARAMETER_LIST_TYPES.map((type) => [type, always]))
2675
- };
2676
- function addNamesUnder(node, names) {
2677
- if (node === null) {
2678
- return;
2679
- }
2680
- if (node.type === "identifier") {
2681
- names.add(node.text);
2682
- return;
2683
- }
2684
- for (const child of bodyStatements(node)) {
2685
- addNamesUnder(child, names);
2686
- }
2687
- }
2688
- function parameterNames2(node, names) {
2689
- for (const parameter of bodyStatements(node)) {
2690
- if (parameter.type === "identifier") {
2691
- names.add(parameter.text);
2692
- continue;
2693
- }
2694
- const declared = field(parameter, "name");
2695
- if (declared !== null && declared.type === "identifier") {
2696
- names.add(declared.text);
2697
- continue;
2698
- }
2699
- addNamesUnder(parameter, names);
2700
- }
2701
- }
2702
- function assignedNames(node, names) {
2703
- addNamesUnder(field(node, "left"), names);
2704
- }
2705
- var BINDS_A_NAME = {
2706
- assignment: assignedNames,
2707
- operator_assignment: assignedNames,
2708
- for: (node, names) => addNamesUnder(field(node, "pattern"), names),
2709
- exception_variable: (node, names) => addNamesUnder(node, names),
2710
- ...Object.fromEntries(
2711
- PARAMETER_LIST_TYPES.map((type) => [type, parameterNames2])
2712
- )
2713
- };
2714
- function localNamesIn(definitionNode) {
2715
- const names = /* @__PURE__ */ new Set();
2716
- const visit = (node) => {
2717
- BINDS_A_NAME[node.type]?.(node, names);
2718
- for (const child of bodyStatements(node)) {
2719
- visit(child);
2720
- }
2721
- };
2722
- visit(definitionNode);
2723
- return names;
2724
- }
2725
- function isBareMethodCall(node, locals) {
2726
- if (node.type !== "identifier" || locals.has(node.text)) {
2727
- return false;
2728
- }
2729
- const parent = node.parent;
2730
- if (parent === null) {
2731
- return false;
2732
- }
2733
- return SPELLS_A_NAME[parent.type]?.(parent, node) !== true;
2734
- }
3037
+ var import_values7 = require("@suss/values");
2735
3038
 
2736
3039
  // src/paths/lowering.ts
2737
3040
  var RAISE_NAMES = /* @__PURE__ */ new Set(["raise", "fail"]);
@@ -3012,30 +3315,28 @@ function lowerRubyBody(body, terminals, responseCalls = []) {
3012
3315
 
3013
3316
  // src/paths/effects.ts
3014
3317
  var RAISE_NAMES2 = /* @__PURE__ */ new Set(["raise", "fail"]);
3015
- var BLOCK_TYPES4 = /* @__PURE__ */ new Set([
3318
+ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
3016
3319
  "body_statement",
3017
3320
  "then",
3018
3321
  "else",
3019
3322
  "do_block",
3020
3323
  "block"
3021
- ]);
3022
- var LITERAL_ARGS = {
3023
- string: (node) => ({
3024
- kind: "string",
3025
- value: node.text.replace(/^["']|["']$/g, "")
3026
- }),
3027
- integer: (node) => {
3028
- const value = Number.parseInt(node.text, 10);
3029
- return Number.isNaN(value) ? null : { kind: "number", value };
3030
- },
3031
- float: (node) => {
3032
- const value = Number.parseFloat(node.text);
3033
- return Number.isNaN(value) ? null : { kind: "number", value };
3034
- },
3035
- true: () => ({ kind: "boolean", value: true }),
3036
- false: () => ({ kind: "boolean", value: false }),
3037
- simple_symbol: (node) => ({ kind: "string", value: node.text.slice(1) })
3038
- };
3324
+ ]);
3325
+ function literalArgOf(node, facts) {
3326
+ const value = evaluatedValue(node, facts);
3327
+ const text2 = (0, import_values7.literalOf)(value);
3328
+ if (text2 !== null) {
3329
+ return { kind: "string", value: text2 };
3330
+ }
3331
+ const constant2 = (0, import_values7.constantOf)(value);
3332
+ if (typeof constant2 === "number") {
3333
+ return { kind: "number", value: constant2 };
3334
+ }
3335
+ if (typeof constant2 === "boolean") {
3336
+ return { kind: "boolean", value: constant2 };
3337
+ }
3338
+ return null;
3339
+ }
3039
3340
  function children2(node) {
3040
3341
  return node.namedChildren.filter((child) => child !== null);
3041
3342
  }
@@ -3119,36 +3420,40 @@ function calleeText(call) {
3119
3420
  }
3120
3421
  return receiver === null ? method : `${receiver.text}.${method}`;
3121
3422
  }
3122
- function argOf(node) {
3123
- const literal = LITERAL_ARGS[node.type]?.(node);
3124
- if (literal !== null && literal !== void 0) {
3423
+ function argOf(node, facts) {
3424
+ const literal = literalArgOf(node, facts);
3425
+ if (literal !== null) {
3125
3426
  return literal;
3126
3427
  }
3127
3428
  if (node.type === "call" && !isArglessReceiverCall(node)) {
3128
- return { kind: "call", callee: calleeText(node), args: argsOf(node) };
3429
+ return {
3430
+ kind: "call",
3431
+ callee: calleeText(node),
3432
+ args: argsOf(node, facts)
3433
+ };
3129
3434
  }
3130
3435
  return { kind: "identifier", name: node.text };
3131
3436
  }
3132
- function argsOf(call) {
3437
+ function argsOf(call, facts) {
3133
3438
  const args = field(call, "arguments");
3134
3439
  if (args === null) {
3135
3440
  return [];
3136
3441
  }
3137
3442
  return children2(args).map(
3138
- (child) => child.type === "pair" ? argOf(field(child, "value") ?? child) : argOf(child)
3443
+ (child) => child.type === "pair" ? argOf(field(child, "value") ?? child, facts) : argOf(child, facts)
3139
3444
  );
3140
3445
  }
3141
3446
  function enclosingStatement(call, body) {
3142
3447
  let current = call;
3143
3448
  while (current.parent !== null) {
3144
- if (BLOCK_TYPES4.has(current.parent.type) || current.parent.id === body.id) {
3449
+ if (BLOCK_TYPES5.has(current.parent.type) || current.parent.id === body.id) {
3145
3450
  return current;
3146
3451
  }
3147
3452
  current = current.parent;
3148
3453
  }
3149
3454
  return current;
3150
3455
  }
3151
- function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS) {
3456
+ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
3152
3457
  const body = field(definitionNode, "body");
3153
3458
  if (body === null) {
3154
3459
  return [];
@@ -3188,7 +3493,7 @@ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, kee
3188
3493
  return {
3189
3494
  type: "invocation",
3190
3495
  callee: calleeText(call),
3191
- args: argsOf(call),
3496
+ args: argsOf(call, facts),
3192
3497
  async: false,
3193
3498
  ...conditions.length > 0 ? { preconditions: conditions } : {}
3194
3499
  };
@@ -3197,7 +3502,7 @@ function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, kee
3197
3502
 
3198
3503
  // src/responseStatus.ts
3199
3504
  var import_extractor3 = require("@suss/extractor");
3200
- var import_values5 = require("@suss/values");
3505
+ var import_values8 = require("@suss/values");
3201
3506
 
3202
3507
  // src/paths/predicates.ts
3203
3508
  var OPERATORS = {
@@ -3266,7 +3571,7 @@ function binaryOf(node) {
3266
3571
  return opaqueOf(node);
3267
3572
  }
3268
3573
  function valueRefOf(node) {
3269
- const literal = literalOf3(node);
3574
+ const literal = literalOf5(node);
3270
3575
  if (literal !== null) {
3271
3576
  return literal;
3272
3577
  }
@@ -3279,7 +3584,7 @@ function valueRefOf(node) {
3279
3584
  }
3280
3585
  return { type: "unresolved", sourceText: node.text };
3281
3586
  }
3282
- function literalOf3(node) {
3587
+ function literalOf5(node) {
3283
3588
  if (node.type === "integer") {
3284
3589
  const value = Number.parseInt(node.text, 10);
3285
3590
  return Number.isNaN(value) ? null : { type: "literal", value };
@@ -3322,13 +3627,13 @@ function statusArgumentOf(call, declaration) {
3322
3627
  }
3323
3628
  return args.positional[declaration.statusArgument] ?? null;
3324
3629
  }
3325
- function statusNumberOf(node, names) {
3326
- const value = evaluatedValue(node);
3327
- const constant2 = (0, import_values5.constantOf)(value);
3630
+ function statusNumberOf(node, names, facts) {
3631
+ const value = evaluatedValue(node, facts);
3632
+ const constant2 = (0, import_values8.constantOf)(value);
3328
3633
  if (typeof constant2 === "number") {
3329
3634
  return constant2;
3330
3635
  }
3331
- const name = (0, import_values5.literalOf)(value);
3636
+ const name = (0, import_values8.literalOf)(value);
3332
3637
  return name === null ? null : names[name] ?? null;
3333
3638
  }
3334
3639
  function declarationsByName(declarations) {
@@ -3372,12 +3677,12 @@ function collectReturns(node, found) {
3372
3677
  }
3373
3678
  return found;
3374
3679
  }
3375
- function readingOfCall(call, declaration, names) {
3680
+ function readingOfCall(call, declaration, names, facts) {
3376
3681
  const argument = statusArgumentOf(call, declaration);
3377
3682
  if (argument === null) {
3378
3683
  return declaration.defaultStatusCode === void 0 ? import_extractor3.absentReading : (0, import_extractor3.writtenReading)(declaration.defaultStatusCode, rangeOf(call));
3379
3684
  }
3380
- const status = statusNumberOf(argument, names);
3685
+ const status = statusNumberOf(argument, names, facts);
3381
3686
  if (status === null) {
3382
3687
  return (0, import_extractor3.unreadableReading)(
3383
3688
  "This response writes a status that does not settle on a number here, so this outcome claims none",
@@ -3521,7 +3826,7 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
3521
3826
  const outcomes = [];
3522
3827
  for (const terminal of terminals) {
3523
3828
  const declaration = declarationOf(terminal, byName, locals);
3524
- const reading = declaration === void 0 ? import_extractor3.absentReading : readingOfCall(terminal, declaration, statusNames);
3829
+ const reading = declaration === void 0 ? import_extractor3.absentReading : readingOfCall(terminal, declaration, statusNames, options.facts);
3525
3830
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
3526
3831
  outcomes.push({
3527
3832
  conditions: path7.map(conditionOf),
@@ -3555,11 +3860,9 @@ function clientCallUnits(root, pack, pattern, options) {
3555
3860
  continue;
3556
3861
  }
3557
3862
  for (const call of callsUnder(method)) {
3558
- const request = requestCall(call, method, root, pattern, options);
3559
- if (request === null) {
3560
- continue;
3863
+ for (const request of requestCalls(call, pattern, options)) {
3864
+ units.push(clientUnit(method, name, request, pattern, pack, options));
3561
3865
  }
3562
- units.push(clientUnit(method, name, request, pattern, pack, options));
3563
3866
  }
3564
3867
  }
3565
3868
  return units;
@@ -3588,36 +3891,60 @@ function callsUnder(node, found = []) {
3588
3891
  }
3589
3892
  return found;
3590
3893
  }
3591
- function requestCall(call, method, root, pattern, options) {
3894
+ function requestCalls(call, pattern, options) {
3592
3895
  const receiver = field(call, "receiver");
3593
3896
  const called = field(call, "method")?.text;
3594
- if (receiver === null || called === void 0) {
3595
- return null;
3897
+ if (receiver === null || called === void 0 || !isLibraryReceiver(receiver, pattern, options)) {
3898
+ return [];
3596
3899
  }
3597
- const prefix = receiverPrefix(receiver, method, root, pattern, options);
3598
- if (prefix === null) {
3599
- return null;
3900
+ const read = (site) => requestCall(
3901
+ call,
3902
+ called,
3903
+ receiverPrefix(receiver, pattern, options, site),
3904
+ pattern,
3905
+ options,
3906
+ site
3907
+ );
3908
+ const plain = read();
3909
+ if (plain !== null && !(0, import_behavioral_ir.hasNameHole)(plain.path)) {
3910
+ return [plain];
3911
+ }
3912
+ const byBoundary = /* @__PURE__ */ new Map();
3913
+ for (const site of constructionSitesOf(call, options.facts)) {
3914
+ const stated = read(site);
3915
+ if (stated !== null) {
3916
+ byBoundary.set(`${stated.method} ${stated.path}`, stated);
3917
+ }
3918
+ }
3919
+ if (byBoundary.size > 0) {
3920
+ return [...byBoundary.values()];
3600
3921
  }
3922
+ return plain === null ? [] : [plain];
3923
+ }
3924
+ function requestCall(call, called, prefix, pattern, options, site) {
3601
3925
  const args = readCallArgs(field(call, "arguments"));
3602
3926
  const verb = pattern.verbMethodNames[called];
3603
3927
  if (verb !== void 0) {
3604
- const path8 = urlIn(args, pattern, options);
3928
+ const path8 = urlIn(args, pattern, options, site);
3605
3929
  return path8 === null ? null : { method: verb, path: prefix + path8 };
3606
3930
  }
3607
3931
  const sent = pattern.requestObject;
3608
3932
  if (sent === void 0 || sent.attribute !== called) {
3609
3933
  return null;
3610
3934
  }
3611
- const built = requestBuilt(args.positional[0], method, sent);
3935
+ const built = requestBuilt(args.positional[0], sent, options, site);
3612
3936
  if (built === null) {
3613
3937
  return null;
3614
3938
  }
3615
- const path7 = pathAt(built.url, options);
3939
+ const path7 = pathAt(built.url, options, site);
3616
3940
  return path7 === null ? null : { method: built.method, path: prefix + path7 };
3617
3941
  }
3618
- function requestBuilt(argument, method, sent) {
3619
- const written = argument === void 0 ? null : assignedCall(argument, method) ?? argument;
3620
- if (written === null || written.type !== "call") {
3942
+ function requestBuilt(argument, sent, options, site) {
3943
+ if (argument === void 0) {
3944
+ return null;
3945
+ }
3946
+ const written = writtenNodeOf(argument, options.facts, site) ?? argument;
3947
+ if (written.type !== "call") {
3621
3948
  return null;
3622
3949
  }
3623
3950
  const requestClass = field(written, "receiver")?.text;
@@ -3628,28 +3955,18 @@ function requestBuilt(argument, method, sent) {
3628
3955
  const args = readCallArgs(field(written, "arguments"));
3629
3956
  return { method: verb, url: args.positional[sent.urlPosition] };
3630
3957
  }
3631
- function assignedCall(node, method) {
3632
- if (node.type !== "identifier") {
3633
- return null;
3634
- }
3635
- const value = assignedValue(node.text, method);
3636
- return value !== null && value.type === "call" ? value : null;
3958
+ function isLibraryReceiver(receiver, pattern, options) {
3959
+ return namesConstant(receiver, pattern.constantName) || builderCallBehind(receiver, pattern, options.facts) !== null;
3637
3960
  }
3638
- function receiverPrefix(receiver, method, root, pattern, options) {
3639
- if (namesConstant(receiver, pattern.constantName)) {
3640
- return "";
3641
- }
3642
- if (receiver.type !== "identifier") {
3643
- return null;
3644
- }
3645
- const built = builtBy(receiver.text, method, root, pattern);
3961
+ function receiverPrefix(receiver, pattern, options, site) {
3962
+ const built = builderCallBehind(receiver, pattern, options.facts);
3646
3963
  if (built === null) {
3647
- return null;
3964
+ return "";
3648
3965
  }
3649
3966
  const args = readCallArgs(field(built, "arguments"));
3650
3967
  const keyword = pattern.builderUrlKeyword;
3651
3968
  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));
3969
+ const path7 = base === void 0 ? null : (0, import_values9.pathOf)(evaluatedValue(base, options.facts, void 0, site));
3653
3970
  return path7 === void 0 || path7 === null ? "" : trimmed(path7);
3654
3971
  }
3655
3972
  function trimmed(path7) {
@@ -3661,61 +3978,28 @@ function namesConstant(receiver, constantName2) {
3661
3978
  }
3662
3979
  return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
3663
3980
  }
3664
- function builtBy(name, method, root, pattern) {
3981
+ function builderCallBehind(receiver, pattern, facts) {
3665
3982
  const builders = pattern.receiverBuilders ?? [];
3666
3983
  if (builders.length === 0) {
3667
3984
  return null;
3668
3985
  }
3669
- const value = assignedValue(name, method) ?? valueMethodNamedReturns(name, root);
3670
- if (value === null || value.type !== "call" || !builders.includes(field(value, "method")?.text ?? "")) {
3986
+ const written = writtenNodeOf(receiver, facts);
3987
+ if (written === null || written.type !== "call" || !builders.includes(field(written, "method")?.text ?? "")) {
3671
3988
  return null;
3672
3989
  }
3673
- const receiver = field(value, "receiver");
3674
- return receiver !== null && namesConstant(receiver, pattern.constantName) ? value : null;
3990
+ const constant2 = field(written, "receiver");
3991
+ return constant2 !== null && namesConstant(constant2, pattern.constantName) ? written : null;
3675
3992
  }
3676
- function valueMethodNamedReturns(name, root) {
3677
- const defined = methodDefinitions(root).find(
3678
- (candidate) => field(candidate, "name")?.text === name
3679
- );
3680
- if (defined === void 0) {
3681
- return null;
3682
- }
3683
- const body = field(defined, "body");
3684
- const statements = body === null ? [] : body.namedChildren;
3685
- const last = statements[statements.length - 1];
3686
- if (last === void 0) {
3687
- return null;
3688
- }
3689
- if (last.type === "operator_assignment" || last.type === "assignment") {
3690
- return field(last, "right");
3691
- }
3692
- return last.type === "return" ? last.namedChildren[0] ?? null : last;
3693
- }
3694
- function assignedValue(name, method) {
3695
- const body = field(method, "body");
3696
- if (body === null) {
3697
- return null;
3698
- }
3699
- for (const statement of runStatements(body)) {
3700
- if (statement.type !== "assignment") {
3701
- continue;
3702
- }
3703
- if (field(statement, "left")?.text === name) {
3704
- return field(statement, "right");
3705
- }
3706
- }
3707
- return null;
3708
- }
3709
- function urlIn(args, pattern, options) {
3993
+ function urlIn(args, pattern, options, site) {
3710
3994
  const keyword = pattern.url.keyword;
3711
3995
  const written = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
3712
- return pathAt(written, options);
3996
+ return pathAt(written, options, site);
3713
3997
  }
3714
- function pathAt(written, options) {
3998
+ function pathAt(written, options, site) {
3715
3999
  if (written === void 0) {
3716
4000
  return null;
3717
4001
  }
3718
- const path7 = (0, import_values6.pathOf)(evaluatedValue(written, options.facts));
4002
+ const path7 = (0, import_values9.pathOf)(evaluatedValue(written, options.facts, void 0, site));
3719
4003
  return path7 === void 0 || (0, import_behavioral_ir.namesNothing)(path7) ? null : path7;
3720
4004
  }
3721
4005
  function clientUnit(method, name, request, pattern, pack, options) {
@@ -3738,15 +4022,15 @@ function clientUnit(method, name, request, pattern, pack, options) {
3738
4022
  recognition: pack.name
3739
4023
  }),
3740
4024
  parameters: [],
3741
- branches: callerBranches(method, range),
4025
+ branches: callerBranches(method, range, options.facts),
3742
4026
  ...responseAccessors(pattern),
3743
4027
  bodyContent: "statements",
3744
4028
  dependencyCalls: [],
3745
4029
  declaredContract: null
3746
4030
  };
3747
4031
  }
3748
- function callerBranches(method, range) {
3749
- const effects = invocationEffects(method);
4032
+ function callerBranches(method, range, facts) {
4033
+ const effects = invocationEffects(method, void 0, void 0, facts);
3750
4034
  return returnPathBranches(method, effects) ?? [returnBranch(range, effects)];
3751
4035
  }
3752
4036
  function responseAccessors(pattern) {
@@ -3884,7 +4168,7 @@ function isDefaultedAt(node) {
3884
4168
  }
3885
4169
 
3886
4170
  // src/filters.ts
3887
- function controllerFilters(pattern, ancestry) {
4171
+ function controllerFilters(pattern, ancestry, read = {}) {
3888
4172
  const forms = pattern.filters ?? [];
3889
4173
  if (forms.length === 0) {
3890
4174
  return [];
@@ -3904,7 +4188,7 @@ function controllerFilters(pattern, ancestry) {
3904
4188
  }
3905
4189
  const form = byName.get(called);
3906
4190
  if (form !== void 0) {
3907
- for (const declaration of declarationsOf(statement, form)) {
4191
+ for (const declaration of declarationsOf(statement, form, read.facts)) {
3908
4192
  declared = [
3909
4193
  ...declared.filter((earlier) => !sameFilter(earlier, declaration)),
3910
4194
  declaration
@@ -3914,12 +4198,12 @@ function controllerFilters(pattern, ancestry) {
3914
4198
  }
3915
4199
  const skipped = skipNames.get(called);
3916
4200
  if (skipped !== void 0) {
3917
- declared = applySkips(declared, skipsOf(statement, skipped));
4201
+ declared = applySkips(declared, skipsOf(statement, skipped, read.facts));
3918
4202
  }
3919
4203
  }
3920
4204
  const resolved = [];
3921
4205
  for (const declaration of declared) {
3922
- const found = methodInAncestry(ancestry, declaration.methodName);
4206
+ const found = methodInAncestry(ancestry, declaration.methodName, read);
3923
4207
  if (found.type !== "found") {
3924
4208
  continue;
3925
4209
  }
@@ -3951,14 +4235,14 @@ function filterReference(filter, displayPath) {
3951
4235
  ...filter.filter.onThrow === true ? { onThrow: true } : {}
3952
4236
  };
3953
4237
  }
3954
- function filterUnit(filter, pattern, displayPath, body) {
4238
+ function filterUnit(filter, pattern, displayPath, body, facts) {
3955
4239
  const range = rangeOf(filter.method);
3956
4240
  const branches = responseBranches(
3957
4241
  filter.method,
3958
4242
  pattern,
3959
4243
  body.effects ?? [],
3960
4244
  body.extraEffects,
3961
- { fallthrough: "handOn" }
4245
+ { fallthrough: "handOn", facts }
3962
4246
  );
3963
4247
  return {
3964
4248
  identity: {
@@ -4006,30 +4290,30 @@ function calledName(statement) {
4006
4290
  }
4007
4291
  return field(statement, "method")?.text ?? null;
4008
4292
  }
4009
- function declarationsOf(statement, filter) {
4293
+ function declarationsOf(statement, filter, facts) {
4010
4294
  const args = readCallArgs(field(statement, "arguments"));
4011
4295
  const keywords = filter.actionKeywords;
4012
- const only = keywords === void 0 ? null : namesUnder(args.keyword[keywords.include]);
4013
- const except = keywords === void 0 ? null : namesUnder(args.keyword[keywords.exclude]);
4014
- return methodNamesOf(args, filter).map((methodName) => ({
4296
+ const only = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.include], facts);
4297
+ const except = keywords === void 0 ? null : actionsUnder(args.keyword[keywords.exclude], facts);
4298
+ return methodNamesOf(args, filter, facts).map((methodName) => ({
4015
4299
  filter,
4016
4300
  methodName,
4017
4301
  only,
4018
4302
  except: except ?? /* @__PURE__ */ new Set()
4019
4303
  }));
4020
4304
  }
4021
- function methodNamesOf(args, filter) {
4305
+ function methodNamesOf(args, filter, facts) {
4022
4306
  if (filter.methodFrom === "withKeyword") {
4023
4307
  const named2 = args.keyword.with;
4024
- const value = named2 === void 0 ? null : symbolValue(named2);
4308
+ const value = named2 === void 0 ? null : stringValueOf(named2, facts);
4025
4309
  return value === null ? [] : [value];
4026
4310
  }
4027
- return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null);
4311
+ return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
4028
4312
  }
4029
- function skipsOf(statement, filterName) {
4313
+ function skipsOf(statement, filterName, facts) {
4030
4314
  const args = readCallArgs(field(statement, "arguments"));
4031
- const actions = namesUnder(args.keyword.only);
4032
- return args.positional.map((arg) => symbolValue(arg)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
4315
+ const actions = actionsUnder(args.keyword.only, facts);
4316
+ return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null).map((methodName) => ({ filterName, methodName, actions }));
4033
4317
  }
4034
4318
  function sameFilter(one, other) {
4035
4319
  return one.filter.name === other.filter.name && one.methodName === other.methodName;
@@ -4053,31 +4337,17 @@ function applySkips(declared, skips) {
4053
4337
  }
4054
4338
  return left;
4055
4339
  }
4056
- function namesUnder(node) {
4340
+ function actionsUnder(node, facts) {
4057
4341
  if (node === void 0) {
4058
4342
  return null;
4059
4343
  }
4060
- const single = symbolValue(node);
4061
- if (single !== null) {
4062
- return /* @__PURE__ */ new Set([single]);
4063
- }
4064
- if (node.type !== "array" && node.type !== "symbol_array") {
4065
- return null;
4066
- }
4067
- const names = /* @__PURE__ */ new Set();
4068
- for (const child of bodyStatements(node)) {
4069
- const bare = child.type === "bare_symbol" ? child.text : null;
4070
- const name = symbolValue(child) ?? bare;
4071
- if (name !== null) {
4072
- names.add(name);
4073
- }
4074
- }
4075
- return names;
4344
+ const found = namesOf(node, facts);
4345
+ return found === null ? null : new Set(found);
4076
4346
  }
4077
4347
 
4078
4348
  // src/storage.ts
4079
4349
  var import_ir_core = require("@suss/ir-core");
4080
- var import_resolution5 = require("@suss/resolution");
4350
+ var import_resolution6 = require("@suss/resolution");
4081
4351
  function children3(node) {
4082
4352
  return node.namedChildren.filter((child) => child !== null);
4083
4353
  }
@@ -4104,7 +4374,7 @@ function constantName(constant2) {
4104
4374
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
4105
4375
  }
4106
4376
  function reachesBase(facts, classKey, bases) {
4107
- (0, import_resolution5.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4377
+ (0, import_resolution6.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4108
4378
  return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4109
4379
  }
4110
4380
  function classBehind(facts, file, constant2) {
@@ -4123,28 +4393,95 @@ function modelPattern(constant2, file, options) {
4123
4393
  (candidate) => reachesBase(options.facts, classKey, candidate.baseClasses)
4124
4394
  );
4125
4395
  }
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");
4135
- if (key !== null) {
4136
- picked.push(key.text.replace(/^:/, "").replace(/:$/, ""));
4137
- }
4396
+ function chainLinks(call) {
4397
+ const links = [];
4398
+ let current = call;
4399
+ while (current !== null && current.type === "call") {
4400
+ links.push(current);
4401
+ current = receiverOf(current);
4402
+ }
4403
+ return links;
4404
+ }
4405
+ function kindOfCall(pattern, method) {
4406
+ if (pattern.writes.includes(method)) {
4407
+ return "write";
4408
+ }
4409
+ if (pattern.reads.includes(method)) {
4410
+ return "read";
4411
+ }
4412
+ return void 0;
4413
+ }
4414
+ function someLibraryDefines(options, method) {
4415
+ return options.patterns.some(
4416
+ (pattern) => kindOfCall(pattern, method) !== void 0
4417
+ );
4418
+ }
4419
+ function argumentsOf2(call) {
4420
+ const args = field(call, "arguments");
4421
+ return args === null ? [] : children3(args);
4422
+ }
4423
+ function recordFieldsOf(argument, facts) {
4424
+ const value = evaluatedValue(argument, facts);
4425
+ return value.kind === "record" ? [...value.fields.keys()] : [];
4426
+ }
4427
+ function keywordKeys(call, facts) {
4428
+ const keys = [];
4429
+ for (const argument of argumentsOf2(call)) {
4430
+ if (argument.type !== "pair") {
4431
+ keys.push(...recordFieldsOf(argument, facts));
4432
+ continue;
4138
4433
  }
4139
- const receiver = receiverOf(current);
4140
- if (receiver !== null) {
4141
- walk(receiver);
4434
+ const key = field(argument, "key");
4435
+ const name = key === null ? null : keyNameOf(key);
4436
+ if (name !== null) {
4437
+ keys.push(name);
4142
4438
  }
4143
- };
4144
- walk(node);
4439
+ }
4440
+ return keys;
4441
+ }
4442
+ function keyNameOf(key) {
4443
+ return hashKeySymbolName(key) ?? symbolValue(key) ?? stringLiteralValue(key);
4444
+ }
4445
+ function hasPositionalArgument(call, facts) {
4446
+ return argumentsOf2(call).some(
4447
+ (argument) => argument.type !== "pair" && recordFieldsOf(argument, facts).length === 0
4448
+ );
4449
+ }
4450
+ function columnsAskedFor(call, pattern, facts) {
4451
+ if (!(pattern.columnArguments ?? []).includes(methodOf(call))) {
4452
+ return [];
4453
+ }
4454
+ return argumentsOf2(call).map((argument) => stringValueOf(argument, facts)).filter((column) => column !== null);
4455
+ }
4456
+ function primaryKeySelector(call, pattern, facts) {
4457
+ const byPrimaryKey = pattern.byPrimaryKey;
4458
+ if (byPrimaryKey === void 0) {
4459
+ return [];
4460
+ }
4461
+ const picks = byPrimaryKey.methods.includes(methodOf(call)) && hasPositionalArgument(call, facts);
4462
+ return picks ? [byPrimaryKey.column] : [];
4463
+ }
4464
+ function selectorOf(call, pattern, facts) {
4465
+ const picked = [];
4466
+ for (const link of chainLinks(call)) {
4467
+ picked.push(...primaryKeySelector(link, pattern, facts));
4468
+ if (pattern.reads.includes(methodOf(link))) {
4469
+ picked.push(...keywordKeys(link, facts));
4470
+ }
4471
+ }
4145
4472
  return [...new Set(picked)];
4146
4473
  }
4147
- function storageEffect(call, container, pattern, kind, selector) {
4474
+ function fieldsOf(call, pattern, kind, facts) {
4475
+ if (kind === "write") {
4476
+ return [...new Set(keywordKeys(call, facts))];
4477
+ }
4478
+ return [
4479
+ ...new Set(
4480
+ chainLinks(call).flatMap((link) => columnsAskedFor(link, pattern, facts))
4481
+ )
4482
+ ];
4483
+ }
4484
+ function storageEffect(call, container, pattern, kind, selector, fields) {
4148
4485
  const operation = methodOf(call);
4149
4486
  return {
4150
4487
  type: "interaction",
@@ -4158,37 +4495,14 @@ function storageEffect(call, container, pattern, kind, selector) {
4158
4495
  interaction: {
4159
4496
  class: "storage-access",
4160
4497
  kind,
4161
- fields: [],
4498
+ fields,
4162
4499
  operation,
4163
4500
  ...selector.length > 0 ? { selector } : {}
4164
4501
  }
4165
4502
  };
4166
4503
  }
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
4504
  function classSettledOn(facts, key) {
4191
- (0, import_resolution5.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4505
+ (0, import_resolution6.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4192
4506
  const objects = new Set(
4193
4507
  facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
4194
4508
  );
@@ -4203,27 +4517,64 @@ function declaredName(facts, classKey) {
4203
4517
  function projectDeclares(facts, classKey, method) {
4204
4518
  return facts.lookup("wantedDeclaredName", 0, classKey).some((row) => String(row[1]) === method);
4205
4519
  }
4206
- function instanceWriteEffects(call, file, options, enclosing) {
4520
+ function constantReceiver(constant2, file, facts) {
4521
+ const classKey = classBehind(facts, file, constant2);
4522
+ return classKey === void 0 ? void 0 : { classKey, container: constantName(constant2) };
4523
+ }
4524
+ function settledReceiver(receiver, file, facts, enclosing) {
4525
+ const classKey = classSettledOn(facts, readKey(file, receiver, enclosing));
4526
+ if (classKey === void 0) {
4527
+ return void 0;
4528
+ }
4529
+ const container = declaredName(facts, classKey);
4530
+ return container === void 0 ? void 0 : { classKey, container };
4531
+ }
4532
+ function receiverClass(call, file, options, enclosing) {
4533
+ const constant2 = rootConstant(call);
4534
+ if (constant2 !== null) {
4535
+ return constantReceiver(constant2, file, options.facts);
4536
+ }
4207
4537
  const receiver = receiverOf(call);
4208
- const method = methodOf(call);
4209
- if (receiver === null || !writeMethods(options.patterns).has(method)) {
4210
- return [];
4538
+ if (receiver === null) {
4539
+ return void 0;
4211
4540
  }
4212
- const classKey = classSettledOn(
4213
- options.facts,
4214
- readKey(file, receiver, enclosing)
4215
- );
4216
- if (classKey === void 0) {
4541
+ return settledReceiver(receiver, file, options.facts, enclosing);
4542
+ }
4543
+ function libraryCallIn(call, options) {
4544
+ return chainLinks(call).find(
4545
+ (link) => someLibraryDefines(options, methodOf(link))
4546
+ ) ?? null;
4547
+ }
4548
+ function modelCallEffects(call, file, options, enclosing) {
4549
+ const worked = libraryCallIn(call, options);
4550
+ if (worked === null) {
4217
4551
  return [];
4218
4552
  }
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)) {
4553
+ const method = methodOf(worked);
4554
+ const target = receiverClass(worked, file, options, enclosing);
4555
+ if (target === void 0) {
4223
4556
  return [];
4224
4557
  }
4225
- const container = declaredName(options.facts, classKey);
4226
- return container === void 0 ? [] : [storageEffect(call, container, pattern, "write", [])];
4558
+ for (const pattern of options.patterns) {
4559
+ const kind = kindOfCall(pattern, method);
4560
+ if (kind === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
4561
+ continue;
4562
+ }
4563
+ if (projectDeclares(options.facts, target.classKey, method)) {
4564
+ return [];
4565
+ }
4566
+ return [
4567
+ storageEffect(
4568
+ worked,
4569
+ target.container,
4570
+ pattern,
4571
+ kind,
4572
+ selectorOf(worked, pattern, options.facts),
4573
+ fieldsOf(worked, pattern, kind, options.facts)
4574
+ )
4575
+ ];
4576
+ }
4577
+ return [];
4227
4578
  }
4228
4579
  function isLoader(node, loader) {
4229
4580
  if (node === null) {
@@ -4261,7 +4612,7 @@ function loaderCallEffects(call, file, options) {
4261
4612
  const pattern = modelPattern(argument, file, options);
4262
4613
  if (pattern !== void 0) {
4263
4614
  effects.push(
4264
- storageEffect(call, constantName(argument), pattern, "read", [])
4615
+ storageEffect(call, constantName(argument), pattern, "read", [], [])
4265
4616
  );
4266
4617
  }
4267
4618
  }
@@ -4269,22 +4620,21 @@ function loaderCallEffects(call, file, options) {
4269
4620
  return effects;
4270
4621
  }
4271
4622
  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);
4623
+ const onModel = modelCallEffects(call, file, options, enclosing);
4624
+ return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
4278
4625
  }
4279
4626
  function storageClaims(call, file, options, enclosing = null) {
4280
4627
  return options.patterns.length > 0 && effectsOfCall(call, file, options, enclosing).length > 0;
4281
4628
  }
4282
4629
  function receiverKeysToAsk(chains, file, options, enclosing) {
4283
- const writes = writeMethods(options.patterns);
4284
4630
  const keys = [];
4285
4631
  for (const call of chains) {
4286
- const receiver = receiverOf(call);
4287
- if (receiver !== null && writes.has(methodOf(call)) && rootConstant(call) === null) {
4632
+ const worked = libraryCallIn(call, options);
4633
+ if (worked === null || rootConstant(worked) !== null) {
4634
+ continue;
4635
+ }
4636
+ const receiver = receiverOf(worked);
4637
+ if (receiver !== null) {
4288
4638
  keys.push(readKey(file, receiver, enclosing));
4289
4639
  }
4290
4640
  }
@@ -4302,7 +4652,7 @@ function storageEffects(calls, file, options, enclosing = null) {
4302
4652
  }
4303
4653
  }
4304
4654
  const chains = calls.filter((call) => !partOfOne.has(call.id));
4305
- (0, import_resolution5.askResolution)(
4655
+ (0, import_resolution6.askResolution)(
4306
4656
  options.facts,
4307
4657
  receiverKeysToAsk(chains, file, options, enclosing),
4308
4658
  "wanted",
@@ -4381,11 +4731,17 @@ function createFileCache(parse, readFile) {
4381
4731
  }
4382
4732
  };
4383
4733
  }
4384
- function fieldReadContext(pattern, cache, fileBlocks, bodyRead) {
4734
+ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
4385
4735
  return {
4386
4736
  pattern,
4387
4737
  cache,
4388
4738
  bodyRead,
4739
+ facts,
4740
+ read: {
4741
+ facts,
4742
+ bodyBlocks: bodyRead.bodyBlocks,
4743
+ dynamicNames: bodyRead.dynamicNames
4744
+ },
4389
4745
  lookup: {
4390
4746
  root: pattern.root,
4391
4747
  pathConvention: pattern.pathConvention,
@@ -4468,7 +4824,13 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
4468
4824
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
4469
4825
  return [];
4470
4826
  }
4471
- const ctx = fieldReadContext(pattern, options.cache, fileBlocks, options);
4827
+ const ctx = fieldReadContext(
4828
+ pattern,
4829
+ options.cache,
4830
+ fileBlocks,
4831
+ options,
4832
+ options.facts
4833
+ );
4472
4834
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
4473
4835
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
4474
4836
  return [];
@@ -4516,7 +4878,11 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4516
4878
  ])) {
4517
4879
  return [];
4518
4880
  }
4519
- const filters = controllerFilters(pattern, ancestry);
4881
+ const filters = controllerFilters(pattern, ancestry, {
4882
+ facts: options.facts,
4883
+ bodyBlocks: options.bodyBlocks,
4884
+ dynamicNames: options.dynamicNames
4885
+ });
4520
4886
  const units = [];
4521
4887
  for (const filter of filters) {
4522
4888
  const displayPath = options.displayPathOf?.(filter.file) ?? filter.file;
@@ -4524,7 +4890,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4524
4890
  filter,
4525
4891
  pattern,
4526
4892
  displayPath,
4527
- bodyOfMethod(filter.method, filter.file, options)
4893
+ bodyOfMethod(filter.method, filter.file, options),
4894
+ options.facts
4528
4895
  );
4529
4896
  units.push(raw);
4530
4897
  options.onReachSeed?.(raw, {
@@ -4561,7 +4928,10 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4561
4928
  });
4562
4929
  };
4563
4930
  const own = /* @__PURE__ */ new Set();
4564
- for (const [actionName, method, block] of publicInstanceMethods(ownBlocks)) {
4931
+ for (const [actionName, method, block] of publicInstanceMethods(
4932
+ ownBlocks,
4933
+ options.bodyBlocks
4934
+ )) {
4565
4935
  own.add(actionName);
4566
4936
  emitAction(actionName, method, block);
4567
4937
  }
@@ -4571,7 +4941,8 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4571
4941
  continue;
4572
4942
  }
4573
4943
  for (const [actionName, method, block] of publicInstanceMethods(
4574
- entry.blocks
4944
+ entry.blocks,
4945
+ options.bodyBlocks
4575
4946
  )) {
4576
4947
  if (seen.has(actionName)) {
4577
4948
  continue;
@@ -4585,14 +4956,17 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
4585
4956
  }
4586
4957
  return units;
4587
4958
  }
4588
- function publicInstanceMethods(blocks) {
4959
+ function publicInstanceMethods(blocks, bodyBlocks) {
4589
4960
  const found = [];
4590
4961
  for (const block of blocks) {
4591
4962
  if (block.info.bodyNode === null) {
4592
4963
  continue;
4593
4964
  }
4594
4965
  const visibility = instanceMethodVisibility(block.info.bodyNode);
4595
- for (const [name, method] of instanceMethodsByName(block.info.bodyNode)) {
4966
+ for (const [name, method] of instanceMethodsByName(
4967
+ block.info.bodyNode,
4968
+ bodyBlocks
4969
+ )) {
4596
4970
  if ((visibility.get(name) ?? "public") === "public") {
4597
4971
  found.push([name, method, block]);
4598
4972
  }
@@ -4608,7 +4982,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
4608
4982
  method,
4609
4983
  pattern,
4610
4984
  body.effects ?? [],
4611
- body.extraEffects
4985
+ body.extraEffects,
4986
+ { facts: bodyRead.facts }
4612
4987
  );
4613
4988
  return {
4614
4989
  identity: {
@@ -4738,7 +5113,8 @@ function bodyOfMethod(method, file, bodyRead = {}) {
4738
5113
  const effects = invocationEffects(
4739
5114
  method,
4740
5115
  bodyRead.inheritedMethods,
4741
- EVERY_ARGLESS_CALL
5116
+ EVERY_ARGLESS_CALL,
5117
+ bodyRead.facts
4742
5118
  );
4743
5119
  const storage = bodyRead.storage;
4744
5120
  const extra = [
@@ -4855,7 +5231,7 @@ async function readFieldShape(symbol, callArgs, scope, ctx, ancestry, range) {
4855
5231
  return {
4856
5232
  contract: literalContract(callArgs, scope, ctx),
4857
5233
  body: bodyFromLookup(
4858
- methodInAncestry(ancestry, symbol),
5234
+ methodInAncestry(ancestry, symbol, ctx.read),
4859
5235
  range,
4860
5236
  "This field",
4861
5237
  NO_METHOD_BEHIND_IT,
@@ -4900,7 +5276,7 @@ async function readWiredClass(ref, scope, ctx) {
4900
5276
  return {
4901
5277
  contract: readClassContract(ancestry, ctx.pattern),
4902
5278
  body: bodyFromLookup(
4903
- methodInAncestry(ancestry, ctx.pattern.resolverMethodName),
5279
+ methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
4904
5280
  range,
4905
5281
  `This field's ${targetQualifiedName}`,
4906
5282
  methodNotSettled(
@@ -5072,6 +5448,202 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
5072
5448
  };
5073
5449
  }
5074
5450
 
5451
+ // src/inflect.ts
5452
+ var SINGULAR_RULES = [
5453
+ [/s$/i, ""],
5454
+ [/(ss)$/i, "$1"],
5455
+ [/(n)ews$/i, "$1ews"],
5456
+ [/([ti])a$/i, "$1um"],
5457
+ [
5458
+ /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i,
5459
+ "$1sis"
5460
+ ],
5461
+ [/(^analy)(sis|ses)$/i, "$1sis"],
5462
+ [/([^f])ves$/i, "$1fe"],
5463
+ [/(hive)s$/i, "$1"],
5464
+ [/(tive)s$/i, "$1"],
5465
+ [/([lr])ves$/i, "$1f"],
5466
+ [/([^aeiouy]|qu)ies$/i, "$1y"],
5467
+ [/(s)eries$/i, "$1eries"],
5468
+ [/(m)ovies$/i, "$1ovie"],
5469
+ [/(x|ch|ss|sh)es$/i, "$1"],
5470
+ [/^(m|l)ice$/i, "$1ouse"],
5471
+ [/(bus)(es)?$/i, "$1"],
5472
+ [/(o)es$/i, "$1"],
5473
+ [/(shoe)s$/i, "$1"],
5474
+ [/(cris|test)(is|es)$/i, "$1is"],
5475
+ [/^(a)x[ie]s$/i, "$1xis"],
5476
+ [/(octop|vir)(us|i)$/i, "$1us"],
5477
+ [/(alias|status)(es)?$/i, "$1"],
5478
+ [/^(ox)en/i, "$1"],
5479
+ [/(vert|ind)ices$/i, "$1ex"],
5480
+ [/(matr)ices$/i, "$1ix"],
5481
+ [/(quiz)zes$/i, "$1"],
5482
+ [/(database)s$/i, "$1"],
5483
+ // The words ActiveSupport calls irregular. It declares them after the
5484
+ // rules above, which is what puts them ahead in the search.
5485
+ [/(p)erson$/i, "$1erson"],
5486
+ [/(p)eople$/i, "$1erson"],
5487
+ [/(m)an$/i, "$1an"],
5488
+ [/(m)en$/i, "$1an"],
5489
+ [/(c)hild$/i, "$1hild"],
5490
+ [/(c)hildren$/i, "$1hild"],
5491
+ [/(s)ex$/i, "$1ex"],
5492
+ [/(s)exes$/i, "$1ex"],
5493
+ [/(m)ove$/i, "$1ove"],
5494
+ [/(m)oves$/i, "$1ove"],
5495
+ [/(z)ombie$/i, "$1ombie"],
5496
+ [/(z)ombies$/i, "$1ombie"]
5497
+ ];
5498
+ var UNCOUNTABLE = /* @__PURE__ */ new Set([
5499
+ "equipment",
5500
+ "information",
5501
+ "rice",
5502
+ "money",
5503
+ "species",
5504
+ "series",
5505
+ "fish",
5506
+ "sheep",
5507
+ "jeans",
5508
+ "police"
5509
+ ]);
5510
+ function singularize(word, project) {
5511
+ if (UNCOUNTABLE.has(word.toLowerCase()) || (project?.uncountable ?? []).some(
5512
+ (uncountable) => uncountable.toLowerCase() === word.toLowerCase()
5513
+ )) {
5514
+ return word;
5515
+ }
5516
+ const own = projectSingular(word, project);
5517
+ if (own !== null) {
5518
+ return own;
5519
+ }
5520
+ for (let at = SINGULAR_RULES.length - 1; at >= 0; at--) {
5521
+ const [pattern, replacement] = SINGULAR_RULES[at];
5522
+ if (pattern.test(word)) {
5523
+ return word.replace(pattern, replacement);
5524
+ }
5525
+ }
5526
+ return word;
5527
+ }
5528
+ function projectSingular(word, project) {
5529
+ const irregular = [...project?.irregular ?? []].reverse();
5530
+ for (const [plural, singular] of irregular) {
5531
+ if (plural.toLowerCase() === word.toLowerCase()) {
5532
+ return matchingCase(word, singular);
5533
+ }
5534
+ }
5535
+ const rules = [...project?.singular ?? []].reverse();
5536
+ for (const [rule3, replacement] of rules) {
5537
+ const applied = applyRule(word, rule3, replacement);
5538
+ if (applied !== null) {
5539
+ return applied;
5540
+ }
5541
+ }
5542
+ return null;
5543
+ }
5544
+ function applyRule(word, rule3, replacement) {
5545
+ const written = /^\/(.*)\/([a-z]*)$/s.exec(rule3);
5546
+ if (written === null) {
5547
+ return word.includes(rule3) ? word.replace(rule3, replacement) : null;
5548
+ }
5549
+ const flags = (written[2] ?? "").replace("m", "s").replace(/[^is]/g, "");
5550
+ const pattern = new RegExp(written[1] ?? "", flags);
5551
+ return pattern.test(word) ? word.replace(pattern, replacement.replace(/\\(\d)/g, "$$$1")) : null;
5552
+ }
5553
+ function matchingCase(word, replacement) {
5554
+ const first = word[0];
5555
+ if (first === void 0 || first !== first.toUpperCase()) {
5556
+ return replacement;
5557
+ }
5558
+ return replacement.charAt(0).toUpperCase() + replacement.slice(1);
5559
+ }
5560
+ function camelize(word, acronyms = []) {
5561
+ return word.split("_").map((part) => capitalize(part, acronyms)).join("");
5562
+ }
5563
+ function capitalize(part, acronyms) {
5564
+ if (part === "") {
5565
+ return "";
5566
+ }
5567
+ const acronym = acronyms.find(
5568
+ (known) => known.toLowerCase() === part.toLowerCase()
5569
+ );
5570
+ return acronym ?? part[0]?.toUpperCase() + part.slice(1);
5571
+ }
5572
+ function associationTargetName(name, plural, project) {
5573
+ return camelize(
5574
+ plural ? singularize(name, project) : name,
5575
+ project?.acronyms ?? []
5576
+ );
5577
+ }
5578
+
5579
+ // src/facts/associations.ts
5580
+ function declaredName2(argument) {
5581
+ if (argument === void 0) {
5582
+ return null;
5583
+ }
5584
+ if (argument.type === "simple_symbol") {
5585
+ return argument.text.slice(1);
5586
+ }
5587
+ return stringLiteralValue(argument);
5588
+ }
5589
+ function targetReference(key, written, nesting) {
5590
+ if (written.startsWith("::")) {
5591
+ return { key, written: written.slice(2), nesting: [] };
5592
+ }
5593
+ return { key, written, nesting };
5594
+ }
5595
+ function targetName(call, calls, name, plural, inflections) {
5596
+ const override = readCallArgs(field(call, "arguments")).keyword[calls.classNameKeyword];
5597
+ if (override === void 0) {
5598
+ return associationTargetName(name, plural, inflections);
5599
+ }
5600
+ return stringLiteralValue(override);
5601
+ }
5602
+ function associationsDeclaredIn(cls, classKey, nesting, calls, inflections) {
5603
+ const body = field(cls, "body");
5604
+ if (body === null || calls.length === 0) {
5605
+ return [];
5606
+ }
5607
+ const found = [];
5608
+ for (const statement of runStatements(body)) {
5609
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
5610
+ continue;
5611
+ }
5612
+ const method = field(statement, "method")?.text ?? "";
5613
+ for (const declaration of calls) {
5614
+ const plural = declaration.plural.includes(method);
5615
+ if (!plural && !declaration.singular.includes(method)) {
5616
+ continue;
5617
+ }
5618
+ const positional = readCallArgs(field(statement, "arguments")).positional;
5619
+ const name = declaredName2(positional[0]);
5620
+ if (name === null) {
5621
+ continue;
5622
+ }
5623
+ const written = targetName(
5624
+ statement,
5625
+ declaration,
5626
+ name,
5627
+ plural,
5628
+ inflections
5629
+ );
5630
+ if (written === null) {
5631
+ continue;
5632
+ }
5633
+ found.push({
5634
+ classKey,
5635
+ name,
5636
+ target: targetReference(
5637
+ `${classKey}#association:${name}`,
5638
+ written,
5639
+ nesting
5640
+ )
5641
+ });
5642
+ }
5643
+ }
5644
+ return found;
5645
+ }
5646
+
5075
5647
  // src/facts/constants.ts
5076
5648
  function children4(node) {
5077
5649
  return node.namedChildren.filter((child) => child !== null);
@@ -5106,20 +5678,32 @@ function isDeclaration(node, parent) {
5106
5678
  }
5107
5679
  return parent.type === "scope_resolution";
5108
5680
  }
5109
- function collectFileConstants(filePath, root) {
5681
+ function collectFileConstants(filePath, root, associationCalls = [], inflections) {
5110
5682
  const definitions = [];
5111
5683
  const references = [];
5684
+ const associations = [];
5112
5685
  const walk = (node, parent, nesting) => {
5113
5686
  if (NESTING_TYPES.has(node.type)) {
5114
5687
  const name = field(node, "name");
5115
5688
  const written = name === null ? null : writtenName(name);
5116
5689
  if (written !== null) {
5690
+ const classKey = nodeId(filePath, node);
5117
5691
  definitions.push({
5118
5692
  qualifiedName: qualify(nesting, written),
5119
- key: nodeId(filePath, node),
5693
+ key: classKey,
5120
5694
  kind: "declaration"
5121
5695
  });
5122
5696
  const inside = [...nesting, ...written.split("::")];
5697
+ for (const declared of associationsDeclaredIn(
5698
+ node,
5699
+ classKey,
5700
+ inside,
5701
+ associationCalls,
5702
+ inflections
5703
+ )) {
5704
+ associations.push(declared);
5705
+ references.push(declared.target);
5706
+ }
5123
5707
  for (const child of children4(node)) {
5124
5708
  walk(child, node, inside);
5125
5709
  }
@@ -5151,7 +5735,7 @@ function collectFileConstants(filePath, root) {
5151
5735
  }
5152
5736
  };
5153
5737
  walk(root, null, []);
5154
- return { filePath, definitions, references };
5738
+ return { filePath, definitions, references, associations };
5155
5739
  }
5156
5740
  function emitConstantBindings(db, perFile) {
5157
5741
  const byName = /* @__PURE__ */ new Map();
@@ -5175,6 +5759,13 @@ function emitConstantBindings(db, perFile) {
5175
5759
  return found.length === 1 ? found[0].key : null;
5176
5760
  };
5177
5761
  for (const file of files) {
5762
+ for (const declared of file.associations) {
5763
+ db.add("declaresAssociation", [
5764
+ declared.classKey,
5765
+ declared.name,
5766
+ declared.target.key
5767
+ ]);
5768
+ }
5178
5769
  for (const reference of file.references) {
5179
5770
  for (let depth = reference.nesting.length; depth >= 0; depth--) {
5180
5771
  const candidate = qualify(
@@ -5199,7 +5790,7 @@ function emitConstantBindings(db, perFile) {
5199
5790
  var import_node_fs4 = __toESM(require("fs"), 1);
5200
5791
  var import_node_path5 = __toESM(require("path"), 1);
5201
5792
  var import_datalog4 = require("@suss/datalog");
5202
- var import_resolution7 = require("@suss/resolution");
5793
+ var import_resolution9 = require("@suss/resolution");
5203
5794
 
5204
5795
  // src/parser.ts
5205
5796
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -5263,6 +5854,7 @@ var import_node_path4 = __toESM(require("path"), 1);
5263
5854
  var import_behavioral_ir5 = require("@suss/behavioral-ir");
5264
5855
  var import_datalog3 = require("@suss/datalog");
5265
5856
  var import_extractor10 = require("@suss/extractor");
5857
+ var import_resolution8 = require("@suss/resolution");
5266
5858
 
5267
5859
  // src/diagnostics.ts
5268
5860
  var import_extractor6 = require("@suss/extractor");
@@ -5314,13 +5906,42 @@ function requiredRelativePath(node) {
5314
5906
  return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
5315
5907
  }
5316
5908
 
5909
+ // src/pack.ts
5910
+ function inflectionsIn(packs) {
5911
+ const pooled = {
5912
+ acronyms: [],
5913
+ irregular: [],
5914
+ uncountable: [],
5915
+ singular: []
5916
+ };
5917
+ for (const pack of packs) {
5918
+ pooled.acronyms?.push(...pack.inflections?.acronyms ?? []);
5919
+ pooled.irregular?.push(...pack.inflections?.irregular ?? []);
5920
+ pooled.uncountable?.push(...pack.inflections?.uncountable ?? []);
5921
+ pooled.singular?.push(...pack.inflections?.singular ?? []);
5922
+ }
5923
+ return pooled;
5924
+ }
5925
+ function bodyBlocksIn(packs) {
5926
+ const pooled = /* @__PURE__ */ new Map();
5927
+ for (const pack of packs) {
5928
+ for (const declared of pack.bodyBlocks ?? []) {
5929
+ pooled.set(declared.name, {
5930
+ moduleOnly: declared.moduleOnly ?? false,
5931
+ definesClassMethods: declared.definesClassMethods ?? false
5932
+ });
5933
+ }
5934
+ }
5935
+ return pooled;
5936
+ }
5937
+
5317
5938
  // src/reach/closure.ts
5318
5939
  var import_behavioral_ir4 = require("@suss/behavioral-ir");
5319
5940
  var import_datalog2 = require("@suss/datalog");
5320
5941
  var import_extractor8 = require("@suss/extractor");
5321
5942
 
5322
5943
  // src/reach/resolveCallee.ts
5323
- var import_resolution6 = require("@suss/resolution");
5944
+ var import_resolution7 = require("@suss/resolution");
5324
5945
  var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
5325
5946
  var INVOKES_RECEIVER = "call";
5326
5947
  var STOP_FOR = {
@@ -5349,7 +5970,7 @@ function calleeSpellings(calls, ctx) {
5349
5970
  }
5350
5971
  return {
5351
5972
  spellingOf,
5352
- outcomes: (0, import_resolution6.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
5973
+ outcomes: (0, import_resolution7.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
5353
5974
  };
5354
5975
  }
5355
5976
  function resolveCallee(call, site, ctx, read) {
@@ -5363,11 +5984,11 @@ function resolveCallee(call, site, ctx, read) {
5363
5984
  return asCallee(spelling, outcomeFor(spelling.key, ctx, read), site, ctx);
5364
5985
  }
5365
5986
  function outcomeFor(key, ctx, read) {
5366
- return read?.outcomes.get(key) ?? (0, import_resolution6.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
5987
+ return read?.outcomes.get(key) ?? (0, import_resolution7.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
5367
5988
  }
5368
5989
  function mightReadAsACall(call, site, ctx) {
5369
5990
  const spelling = spellingFor(call, site);
5370
- return spelling.kind === "receiver" && (0, import_resolution6.couldBeSettled)(ctx.facts, spelling.key);
5991
+ return spelling.kind === "receiver" && (0, import_resolution7.couldBeSettled)(ctx.facts, spelling.key);
5371
5992
  }
5372
5993
  function readsAsACall(call, site, ctx, read) {
5373
5994
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
@@ -5478,7 +6099,11 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
5478
6099
  if (ancestry === void 0) {
5479
6100
  return stop("outsideRun");
5480
6101
  }
5481
- const found = methodInAncestry(ancestry, methodName);
6102
+ const found = methodInAncestry(ancestry, methodName, {
6103
+ facts: ctx.facts,
6104
+ bodyBlocks: ctx.bodyBlocks,
6105
+ dynamicNames: ctx.dynamicNames
6106
+ });
5482
6107
  if (found.type === "found") {
5483
6108
  return followed(reachedMethod(found.method, found.block, methodName));
5484
6109
  }
@@ -5507,7 +6132,10 @@ function singletonMethodOn(qualifiedName, methodName, ctx) {
5507
6132
  if (block.info.bodyNode === null) {
5508
6133
  continue;
5509
6134
  }
5510
- const found = singletonMethodsByName(block.info.bodyNode).get(methodName);
6135
+ const found = singletonMethodsByName(
6136
+ block.info.bodyNode,
6137
+ ctx.bodyBlocks
6138
+ ).get(methodName);
5511
6139
  if (found !== void 0) {
5512
6140
  return followed(reachedMethod(found, block, methodName));
5513
6141
  }
@@ -5861,7 +6489,7 @@ function parameterName(param) {
5861
6489
 
5862
6490
  // src/reach/context.ts
5863
6491
  var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
5864
- async function buildReachContext(files, facts) {
6492
+ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map()) {
5865
6493
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
5866
6494
  const classes = [];
5867
6495
  const classNames = /* @__PURE__ */ new Map();
@@ -5935,7 +6563,9 @@ async function buildReachContext(files, facts) {
5935
6563
  topLevelMethods,
5936
6564
  facts,
5937
6565
  classNames,
5938
- definitions
6566
+ definitions,
6567
+ bodyBlocks,
6568
+ dynamicNames
5939
6569
  };
5940
6570
  }
5941
6571
  function namedChildren(node) {
@@ -5981,20 +6611,48 @@ function inheritedMethodsIn(packs) {
5981
6611
  }
5982
6612
  return found;
5983
6613
  }
5984
- function emitStorageFacts(db, packs) {
5985
- for (const pack of packs) {
5986
- for (const pattern of pack.storage ?? []) {
5987
- for (const base of pattern.baseClasses) {
5988
- for (const method of pattern.givesBack) {
5989
- db.add("givesBackOne", [base, method]);
5990
- }
5991
- }
5992
- }
5993
- }
6614
+ function associationCallsIn(packs) {
6615
+ return packs.flatMap(
6616
+ (pack) => (pack.storage ?? []).flatMap(
6617
+ (pattern) => pattern.associations === void 0 ? [] : [pattern.associations]
6618
+ )
6619
+ );
6620
+ }
6621
+ function packWordsOf(packs) {
6622
+ return {
6623
+ givesBackOne: packs.flatMap(
6624
+ (pack) => (pack.storage ?? []).flatMap(
6625
+ (pattern) => pattern.baseClasses.flatMap(
6626
+ (base) => pattern.givesBack.map((method) => ({ base, method }))
6627
+ )
6628
+ )
6629
+ )
6630
+ };
6631
+ }
6632
+ function factsForFile(options) {
6633
+ const db = new import_datalog3.Database();
6634
+ const bodyBlocks = bodyBlocksIn(options.packs);
6635
+ emitValueFacts(db, options.file, options.root, bodyBlocks);
6636
+ emitConstantBindings(db, [
6637
+ collectFileConstants(
6638
+ options.file,
6639
+ options.root,
6640
+ associationCallsIn(options.packs),
6641
+ inflectionsIn(options.packs)
6642
+ )
6643
+ ]);
6644
+ emitRequireFacts(db, options.file, options.root, /* @__PURE__ */ new Set([options.file]));
6645
+ bindEvaluator(db, {
6646
+ files: [{ file: options.file, root: options.root }],
6647
+ definitions: new Map(methodDefinitionsIn(options.file, options.root))
6648
+ });
6649
+ (0, import_resolution8.addPackWords)(db, packWordsOf(options.packs));
6650
+ return db;
5994
6651
  }
5995
6652
  function alreadyDiscovered(seen, raw) {
5996
6653
  const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
5997
- const key = `${raw.identity.file}::${reported}::${raw.identity.range.start}`;
6654
+ const boundary = raw.boundaryBinding === null ? "" : (0, import_behavioral_ir5.boundaryKey)(raw.boundaryBinding) ?? "";
6655
+ const key = `${raw.identity.file}::${reported}::${raw.identity.range.start}::${boundary}`;
5998
6656
  if (seen.has(key)) {
5999
6657
  return true;
6000
6658
  }
@@ -6035,6 +6693,9 @@ async function extractRubyProject(options) {
6035
6693
  );
6036
6694
  const parsed = [];
6037
6695
  const definitions = /* @__PURE__ */ new Map();
6696
+ const associationCalls = associationCallsIn(options.packs);
6697
+ const bodyBlocks = bodyBlocksIn(options.packs);
6698
+ const inflections = inflectionsIn(options.packs);
6038
6699
  for (const file of options.files) {
6039
6700
  await timer.timeAsync("parse", async () => {
6040
6701
  const root = await cache.get(file);
@@ -6042,21 +6703,27 @@ async function extractRubyProject(options) {
6042
6703
  return;
6043
6704
  }
6044
6705
  parsed.push({ file, root });
6045
- emitValueFacts(db, file, root);
6706
+ emitValueFacts(db, file, root, bodyBlocks);
6046
6707
  for (const [key, method] of methodDefinitionsIn(file, root)) {
6047
6708
  definitions.set(key, method);
6048
6709
  }
6049
- constants.push(collectFileConstants(file, root));
6710
+ constants.push(
6711
+ collectFileConstants(file, root, associationCalls, inflections)
6712
+ );
6050
6713
  });
6051
6714
  }
6052
- timer.time("discover", () => {
6715
+ const dynamicNames = timer.time("discover", () => {
6053
6716
  emitConstantBindings(db, constants);
6054
6717
  const known = new Set(parsed.map(({ file }) => file));
6055
6718
  for (const { file, root } of parsed) {
6056
6719
  emitRequireFacts(db, file, root, known);
6057
6720
  }
6058
6721
  bindEvaluator(db, { files: parsed, definitions });
6059
- emitStorageFacts(db, options.packs);
6722
+ (0, import_resolution8.addPackWords)(db, packWordsOf(options.packs));
6723
+ return readDynamicNames(
6724
+ db,
6725
+ new Map(parsed.map(({ file, root }) => [file, root]))
6726
+ );
6060
6727
  });
6061
6728
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
6062
6729
  const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
@@ -6064,7 +6731,7 @@ async function extractRubyProject(options) {
6064
6731
  const inheritedMethods = inheritedMethodsIn(options.packs);
6065
6732
  const reachContext = await timer.timeAsync(
6066
6733
  "discover",
6067
- () => buildReachContext(parsed, db)
6734
+ () => buildReachContext(parsed, db, bodyBlocks, dynamicNames)
6068
6735
  );
6069
6736
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
6070
6737
  const seeds = [];
@@ -6084,6 +6751,8 @@ async function extractRubyProject(options) {
6084
6751
  cache,
6085
6752
  ...storage === void 0 ? {} : { storage },
6086
6753
  inheritedMethods,
6754
+ bodyBlocks,
6755
+ dynamicNames,
6087
6756
  displayPathOf,
6088
6757
  facts: db,
6089
6758
  onReachSeed: (raw, seed) => seedByRaw.set(raw, seed)
@@ -6155,7 +6824,9 @@ async function extractRubyProject(options) {
6155
6824
  context: reachContext,
6156
6825
  displayPathOf,
6157
6826
  ...storage === void 0 ? {} : { storage },
6158
- inheritedMethods
6827
+ inheritedMethods,
6828
+ bodyBlocks,
6829
+ dynamicNames
6159
6830
  })
6160
6831
  );
6161
6832
  for (const entry of found) {
@@ -6272,7 +6943,7 @@ function findRubyFiles(root) {
6272
6943
  }
6273
6944
 
6274
6945
  // src/facts/why.ts
6275
- var WITNESS_RULES = [...import_resolution7.RESOLUTION_RULES, ...RUBY_RULES];
6946
+ var WITNESS_RULES = [...import_resolution9.RESOLUTION_RULES, ...RUBY_RULES];
6276
6947
  var METHOD_TYPES5 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
6277
6948
  function namedChildrenOf(node) {
6278
6949
  return node.namedChildren.filter((child) => child !== null);
@@ -6321,6 +6992,10 @@ function displayNameOf(node) {
6321
6992
  function width(node) {
6322
6993
  return node.endIndex - node.startIndex;
6323
6994
  }
6995
+ function askedKey(value) {
6996
+ const enclosing = enclosingMethod2(value.node);
6997
+ return calleeKeyOf(value.file, value.node, enclosing) ?? readKey(value.file, value.node, enclosing);
6998
+ }
6324
6999
  var RubyWhySession = class {
6325
7000
  root;
6326
7001
  db = new import_datalog4.Database();
@@ -6393,10 +7068,10 @@ var RubyWhySession = class {
6393
7068
  * which the caller says in its own words.
6394
7069
  */
6395
7070
  explain(value, options = {}) {
6396
- return (0, import_resolution7.explainResolvedKey)({
7071
+ return (0, import_resolution9.explainResolvedKey)({
6397
7072
  db: this.db,
6398
7073
  rules: WITNESS_RULES,
6399
- key: readKey(value.file, value.node, enclosingMethod2(value.node)),
7074
+ key: askedKey(value),
6400
7075
  locate: (key) => this.locate(key),
6401
7076
  displayPath: (key) => this.displayPath(key),
6402
7077
  ...options
@@ -6514,6 +7189,7 @@ async function rubyStubEvidence(options) {
6514
7189
  emitValueFacts,
6515
7190
  evaluatedValue,
6516
7191
  extractRubyProject,
7192
+ factsForFile,
6517
7193
  field,
6518
7194
  findRubyFiles,
6519
7195
  graphqlTypeNameFromQualified,