@suss/adapter-ruby 0.31.2 → 0.33.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
@@ -64,6 +64,8 @@ __export(index_exports, {
64
64
  resolveConstantFile: () => resolveConstantFile,
65
65
  rubyStubEvidence: () => rubyStubEvidence,
66
66
  runStatements: () => runStatements,
67
+ storageClaims: () => storageClaims,
68
+ storageEffects: () => storageEffects,
67
69
  stringLiteralValue: () => stringLiteralValue,
68
70
  stringValueOf: () => stringValueOf,
69
71
  symbolValue: () => symbolValue,
@@ -143,7 +145,25 @@ var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
143
145
  "module",
144
146
  "singleton_class"
145
147
  ]);
148
+ var PROGRAM_TYPE = "program";
149
+ var MODULE_SCOPE_STOPS = /* @__PURE__ */ new Set([
150
+ ...OWN_BODY_TYPES,
151
+ "block",
152
+ "do_block"
153
+ ]);
146
154
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
155
+ var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
156
+ var LAMBDA_TYPE = "lambda";
157
+ function enclosingDefinition(node) {
158
+ let current = node.parent;
159
+ while (current !== null) {
160
+ if (METHOD_TYPES.has(current.type) || current.type === LAMBDA_TYPE) {
161
+ return current;
162
+ }
163
+ current = current.parent;
164
+ }
165
+ return null;
166
+ }
147
167
  var INCLUDE_CALL = "include";
148
168
  var PREPEND_CALL = "prepend";
149
169
  var ARGUMENT_LIST_TYPE = "argument_list";
@@ -460,8 +480,14 @@ var RUBY_PROGRAM = (0, import_resolution.resolutionProgram)(RUBY_RULES);
460
480
  function resolveValues(db, keys) {
461
481
  (0, import_resolution.askResolution)(db, keys, "wanted", RUBY_PROGRAM);
462
482
  }
483
+ function resolveEnvObjects(db, objects) {
484
+ (0, import_resolution.askResolution)(db, objects, "wantedEnvObject", RUBY_PROGRAM);
485
+ }
463
486
  function resolvedFunctions(db, key) {
464
- return db.facts("wantedResolves").filter((row) => String(row[0]) === key).map((row) => String(row[1]));
487
+ return [
488
+ ...db.lookup("wantedResolves", 0, key),
489
+ ...db.lookup("wantedGivesBack", 0, key)
490
+ ].map((row) => String(row[1]));
465
491
  }
466
492
  function writtenValueOf(db, key) {
467
493
  resolveValues(db, [key]);
@@ -561,12 +587,15 @@ var BINDS_A_NAME = {
561
587
  PARAMETER_LIST_TYPES.map((type) => [type, parameterNames])
562
588
  )
563
589
  };
564
- function localNamesIn(definitionNode) {
590
+ var DESCENDS_EVERYWHERE = /* @__PURE__ */ new Set();
591
+ function localNamesIn(definitionNode, stops = DESCENDS_EVERYWHERE) {
565
592
  const names = /* @__PURE__ */ new Set();
566
593
  const visit = (node) => {
567
594
  BINDS_A_NAME[node.type]?.(node, names);
568
595
  for (const child of bodyStatements(node)) {
569
- visit(child);
596
+ if (!stops.has(child.type)) {
597
+ visit(child);
598
+ }
570
599
  }
571
600
  };
572
601
  visit(definitionNode);
@@ -579,11 +608,12 @@ function spellsAName(node) {
579
608
  }
580
609
  return SPELLS_A_NAME[parent.type]?.(parent, node) === true;
581
610
  }
611
+ var KEYWORD_LITERALS = /* @__PURE__ */ new Set(["__FILE__", "__LINE__", "__ENCODING__"]);
582
612
  function isBareMethodCall(node, locals) {
583
613
  if (node.type !== "identifier" || locals.has(node.text)) {
584
614
  return false;
585
615
  }
586
- return !spellsAName(node);
616
+ return !KEYWORD_LITERALS.has(node.text) && !spellsAName(node);
587
617
  }
588
618
 
589
619
  // src/facts/locals.ts
@@ -734,25 +764,46 @@ function namesOfFile(tree) {
734
764
  cache.file ??= scopeNames(/* @__PURE__ */ new Set(), tree.rootNode);
735
765
  return cache.file;
736
766
  }
767
+ function scopesAround(enclosing) {
768
+ const chain = [];
769
+ let current = enclosing;
770
+ while (current !== null && current.type === LAMBDA_TYPE) {
771
+ chain.push(current);
772
+ current = enclosingDefinition(current);
773
+ }
774
+ chain.push(current);
775
+ return chain;
776
+ }
777
+ var namesOfScope = (scope, node) => scope === null ? namesOfFile(node.tree) : namesOfMethod(scope);
737
778
  function ownerOfName(node, name, enclosing) {
738
779
  if (node.type === "constant") {
739
780
  return null;
740
781
  }
741
- const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
742
- if (names.blockParams.has(name)) {
743
- const block = blockDeclaring(node, name);
744
- if (block !== null) {
745
- return block;
782
+ for (const scope of scopesAround(enclosing)) {
783
+ const names = namesOfScope(scope, node);
784
+ if (names.blockParams.has(name)) {
785
+ const block = blockDeclaring(node, name);
786
+ if (block !== null) {
787
+ return block;
788
+ }
789
+ }
790
+ if (scope !== null && names.locals.has(name)) {
791
+ return scope;
746
792
  }
747
793
  }
748
- return enclosing !== null && names.locals.has(name) ? enclosing : null;
794
+ return null;
749
795
  }
750
796
  function isLocalName(node, name, enclosing) {
751
- const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
752
- if (names.blockParams.has(name) && blockDeclaring(node, name) !== null) {
753
- return true;
797
+ for (const scope of scopesAround(enclosing)) {
798
+ const names = namesOfScope(scope, node);
799
+ if (names.blockParams.has(name) && blockDeclaring(node, name) !== null) {
800
+ return true;
801
+ }
802
+ if (names.locals.has(name)) {
803
+ return true;
804
+ }
754
805
  }
755
- return names.locals.has(name);
806
+ return false;
756
807
  }
757
808
  function blockDeclaring(node, name) {
758
809
  let current = node.parent;
@@ -943,19 +994,33 @@ function calleeKeyOf(filePath, node, enclosing) {
943
994
  }
944
995
  return field(node, "receiver") === null ? nameKey(filePath, method, enclosing) : nodeId(filePath, method);
945
996
  }
997
+ function invokedKeyOf(filePath, node, enclosing) {
998
+ const invoked = invokedValueOf(node);
999
+ return invoked === null ? null : readKey(filePath, invoked, enclosing);
1000
+ }
946
1001
  function emitCall(emitter, call) {
947
- add(emitter, "writtenValue", nodeId(emitter.filePath, call));
1002
+ const callKey = nodeId(emitter.filePath, call);
1003
+ add(emitter, "writtenValue", callKey);
1004
+ const invoked = invokedValueOf(call);
1005
+ if (invoked !== null) {
1006
+ add(emitter, "call", callKey, valueKey(emitter, invoked));
1007
+ }
1008
+ if (!emitMessageSent(emitter, call, callKey) && invoked === null) {
1009
+ return;
1010
+ }
1011
+ emitCallArguments(emitter, call, callKey);
1012
+ }
1013
+ function emitMessageSent(emitter, call, callKey) {
948
1014
  const method = call.type === "identifier" ? call : field(call, "method");
949
1015
  const calleeKey = calleeKeyOf(emitter.filePath, call, emitter.enclosing);
950
1016
  if (method === null || calleeKey === null) {
951
- return;
1017
+ return false;
952
1018
  }
953
- const receiver = field(call, "receiver");
954
- const callKey = nodeId(emitter.filePath, call);
955
1019
  add(emitter, "call", callKey, calleeKey);
956
1020
  if (!emitter.insideMethod) {
957
1021
  add(emitter, "callOutsideMethod", callKey);
958
1022
  }
1023
+ const receiver = field(call, "receiver");
959
1024
  if (receiver !== null) {
960
1025
  add(
961
1026
  emitter,
@@ -967,6 +1032,21 @@ function emitCall(emitter, call) {
967
1032
  } else if (emitter.selfKey !== null) {
968
1033
  add(emitter, "readsProperty", calleeKey, emitter.selfKey, method.text);
969
1034
  }
1035
+ return true;
1036
+ }
1037
+ var INVOKE_METHOD = "call";
1038
+ function invokedValueOf(call) {
1039
+ if (call.type !== "call") {
1040
+ return null;
1041
+ }
1042
+ const receiver = field(call, "receiver");
1043
+ if (receiver === null) {
1044
+ return null;
1045
+ }
1046
+ const method = field(call, "method");
1047
+ return method === null || method.text === INVOKE_METHOD ? receiver : null;
1048
+ }
1049
+ function emitCallArguments(emitter, call, callKey) {
970
1050
  const args = field(call, "arguments");
971
1051
  let position = 0;
972
1052
  for (const argument of args === null ? [] : children(args)) {
@@ -1086,12 +1166,58 @@ function walkExpressions(node, emitter, visit, turns = []) {
1086
1166
  );
1087
1167
  }
1088
1168
  }
1169
+ var WRITTEN_KEY_TYPES = /* @__PURE__ */ new Set([
1170
+ "string",
1171
+ "integer",
1172
+ "simple_symbol",
1173
+ "bare_string",
1174
+ "bare_symbol"
1175
+ ]);
1176
+ function emitKeyedRead(emitter, site, container, key) {
1177
+ if (WRITTEN_KEY_TYPES.has(key.type)) {
1178
+ return;
1179
+ }
1180
+ add(
1181
+ emitter,
1182
+ "readsKeyed",
1183
+ nodeId(emitter.filePath, site),
1184
+ valueKey(emitter, container),
1185
+ valueKey(emitter, key)
1186
+ );
1187
+ }
1188
+ function emitKeyedElement(emitter, node) {
1189
+ const object = field(node, "object");
1190
+ if (object === null || isWriteTarget(node)) {
1191
+ return;
1192
+ }
1193
+ const index = node.namedChildren.find(
1194
+ (child) => child !== null && child.id !== object.id
1195
+ );
1196
+ if (index !== void 0) {
1197
+ emitKeyedRead(emitter, node, object, index);
1198
+ }
1199
+ }
1200
+ function emitKeyedFetch(emitter, call) {
1201
+ const receiver = field(call, "receiver");
1202
+ if (receiver === null || field(call, "method")?.text !== "fetch") {
1203
+ return;
1204
+ }
1205
+ const { positional } = readCallArgs(field(call, "arguments"));
1206
+ const key = positional[0];
1207
+ if (key !== void 0) {
1208
+ emitKeyedRead(emitter, call, receiver, key);
1209
+ }
1210
+ }
1089
1211
  function emitExpressionFacts(emitter, node) {
1090
1212
  emitDynamicDefinition(emitter, node, []);
1091
1213
  walkExpressions(node, emitter, (child, turns) => {
1092
1214
  emitDynamicDefinition(emitter, child, turns);
1093
1215
  if (child.type === "call" || isBareCall(child, emitter.enclosing)) {
1094
1216
  emitCall(emitter, child);
1217
+ emitKeyedFetch(emitter, child);
1218
+ }
1219
+ if (child.type === "element_reference") {
1220
+ emitKeyedElement(emitter, child);
1095
1221
  }
1096
1222
  if (ARRAY_TYPES.has(child.type)) {
1097
1223
  emitArray(emitter, child);
@@ -1215,6 +1341,25 @@ function assignedValueOf(node) {
1215
1341
  }
1216
1342
  return field(node, "right");
1217
1343
  }
1344
+ function emitLambdasIn(emitter, body) {
1345
+ for (const child of children(body)) {
1346
+ if (child.type === LAMBDA_TYPE) {
1347
+ emitMethodFacts(emitter, child);
1348
+ continue;
1349
+ }
1350
+ if (OWN_BODY_TYPES.has(child.type)) {
1351
+ continue;
1352
+ }
1353
+ emitLambdasIn(emitter, child);
1354
+ }
1355
+ }
1356
+ function definitionBody(definition) {
1357
+ const body = field(definition, "body");
1358
+ if (body === null || definition.type !== LAMBDA_TYPE) {
1359
+ return body;
1360
+ }
1361
+ return field(body, "body");
1362
+ }
1218
1363
  function emitMethodFacts(emitter, method) {
1219
1364
  const funcKey = nodeId(emitter.filePath, method);
1220
1365
  add(emitter, "func", funcKey);
@@ -1229,7 +1374,7 @@ function emitMethodFacts(emitter, method) {
1229
1374
  position += 1;
1230
1375
  }
1231
1376
  const inside = { ...emitter, enclosing: method };
1232
- const body = field(method, "body");
1377
+ const body = definitionBody(method);
1233
1378
  if (body === null) {
1234
1379
  return funcKey;
1235
1380
  }
@@ -1261,6 +1406,7 @@ function emitMethodFacts(emitter, method) {
1261
1406
  }
1262
1407
  emitExpressionFacts(inside, body);
1263
1408
  emitScopeWrites(inside, method, body);
1409
+ emitLambdasIn(inside, body);
1264
1410
  return funcKey;
1265
1411
  }
1266
1412
  function isConstruction(value) {
@@ -1340,7 +1486,6 @@ function emitCandidates(emitter, key, group) {
1340
1486
  add(emitter, "writesAllStated", key);
1341
1487
  }
1342
1488
  }
1343
- var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1344
1489
  var INITIALIZE_METHOD = "initialize";
1345
1490
  function receiverKeyOf(filePath, method, classKey) {
1346
1491
  return method.type === "singleton_method" ? classKey : `${nodeId(filePath, method)}#self`;
@@ -1388,7 +1533,7 @@ function emitClassFacts(emitter, cls) {
1388
1533
  left.text,
1389
1534
  valueKey(emitter, right)
1390
1535
  );
1391
- emitExpressionFacts(emitter, statement);
1536
+ emitExpressionFacts(within, statement);
1392
1537
  }
1393
1538
  continue;
1394
1539
  }
@@ -1421,12 +1566,15 @@ function emitClassFacts(emitter, cls) {
1421
1566
  add(emitter, "initializes", classKey, funcKey);
1422
1567
  }
1423
1568
  }
1424
- const methodBody = field(statement, "body");
1425
- if (methodBody !== null) {
1426
- emitInstanceStores(within, funcKey, methodBody, stored);
1569
+ const methodBody2 = field(statement, "body");
1570
+ if (methodBody2 !== null) {
1571
+ emitInstanceStores(within, funcKey, methodBody2, stored);
1427
1572
  }
1428
1573
  }
1429
1574
  emitInstanceWrites(within, classKey, collected);
1575
+ if (body !== null) {
1576
+ emitLambdasIn(within, body);
1577
+ }
1430
1578
  return classKey;
1431
1579
  }
1432
1580
  function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
@@ -1462,6 +1610,7 @@ function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
1462
1610
  walk(root);
1463
1611
  emitExpressionFacts(emitter, root);
1464
1612
  emitScopeWrites(emitter, null, root);
1613
+ emitLambdasIn(emitter, root);
1465
1614
  }
1466
1615
 
1467
1616
  // src/values/lowering.ts
@@ -2451,7 +2600,6 @@ var evaluators = /* @__PURE__ */ new WeakMap();
2451
2600
  var contexts = /* @__PURE__ */ new WeakMap();
2452
2601
  var withoutFacts = /* @__PURE__ */ new WeakMap();
2453
2602
  var filesByTree = /* @__PURE__ */ new WeakMap();
2454
- var METHOD_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
2455
2603
  function bindEvaluator(db, nodes) {
2456
2604
  for (const entry of nodes.files) {
2457
2605
  filesByTree.set(entry.root.tree, entry.file);
@@ -2464,6 +2612,24 @@ function writtenNodeOf(node, db, site) {
2464
2612
  const context = db === void 0 ? void 0 : contexts.get(db);
2465
2613
  return context === void 0 ? null : context.writtenTo(node, site);
2466
2614
  }
2615
+ function askWrittenValues(nodes, db) {
2616
+ if (db === void 0 || !contexts.has(db)) {
2617
+ return;
2618
+ }
2619
+ const keys = /* @__PURE__ */ new Set();
2620
+ for (const node of nodes) {
2621
+ const file = filesByTree.get(node.tree);
2622
+ if (file !== void 0) {
2623
+ keys.add(readKey(file, node, enclosingDefinition(node)));
2624
+ }
2625
+ }
2626
+ if (keys.size === 0) {
2627
+ return;
2628
+ }
2629
+ const asked = [...keys];
2630
+ resolveValues(db, asked);
2631
+ (0, import_resolution4.writtenValuesByKey)(db, asked, (more) => resolveValues(db, more));
2632
+ }
2467
2633
  function evaluatedValue(node, db, bindings, site) {
2468
2634
  const evaluator = evaluatorFor(node, db);
2469
2635
  if (bindings === void 0) {
@@ -2505,7 +2671,7 @@ function stringValueOf(node, db, bindings) {
2505
2671
  function methodDefinitionsIn(file, root) {
2506
2672
  const found = /* @__PURE__ */ new Map();
2507
2673
  const visit = (node) => {
2508
- if (METHOD_TYPES3.has(node.type)) {
2674
+ if (METHOD_TYPES.has(node.type)) {
2509
2675
  found.set(nodeId(file, node), node);
2510
2676
  }
2511
2677
  for (const child of node.namedChildren) {
@@ -2530,16 +2696,6 @@ function evaluatorFor(node, db) {
2530
2696
  }
2531
2697
  return local;
2532
2698
  }
2533
- function enclosingMethod(node) {
2534
- let current = node.parent;
2535
- while (current !== null) {
2536
- if (METHOD_TYPES3.has(current.type)) {
2537
- return current;
2538
- }
2539
- current = current.parent;
2540
- }
2541
- return null;
2542
- }
2543
2699
  function contextOver(db, nodes) {
2544
2700
  const filesByRoot = /* @__PURE__ */ new Map();
2545
2701
  const rootsByFile = /* @__PURE__ */ new Map();
@@ -2554,7 +2710,7 @@ function contextOver(db, nodes) {
2554
2710
  if (file === null) {
2555
2711
  return null;
2556
2712
  }
2557
- const key = readKey(file, node, enclosingMethod(node));
2713
+ const key = readKey(file, node, enclosingDefinition(node));
2558
2714
  const answer = site === void 0 ? writtenValueOf(db, key) : writtenValueUnder(db, key, site);
2559
2715
  return answer === null ? null : (0, import_resolution4.nodeOfKey)(rootsByFile, answer);
2560
2716
  },
@@ -2564,7 +2720,7 @@ function contextOver(db, nodes) {
2564
2720
  if (file === null || method === null) {
2565
2721
  return null;
2566
2722
  }
2567
- const key = field(call, "receiver") === null ? readKey(file, method, enclosingMethod(call)) : nodeId(file, method);
2723
+ const key = field(call, "receiver") === null ? readKey(file, method, enclosingDefinition(call)) : nodeId(file, method);
2568
2724
  resolveValues(db, [key]);
2569
2725
  const resolved = resolvedFunctions(db, key);
2570
2726
  const settled = resolved.length === 1 ? resolved[0] : void 0;
@@ -2716,8 +2872,8 @@ function qualifyConstantRef(node, nesting) {
2716
2872
  return compoundName(node);
2717
2873
  }
2718
2874
  if (node.type === "constant") {
2719
- const innermost = nesting[0] ?? null;
2720
- return innermost === null ? node.text : `${innermost}::${node.text}`;
2875
+ const innermost2 = nesting[0] ?? null;
2876
+ return innermost2 === null ? node.text : `${innermost2}::${node.text}`;
2721
2877
  }
2722
2878
  return null;
2723
2879
  }
@@ -2774,9 +2930,9 @@ function ownIdentity(nameNode, outerNesting) {
2774
2930
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
2775
2931
  }
2776
2932
  function definedName(nameNode, outerNesting) {
2777
- const innermost = outerNesting[0] ?? null;
2778
- if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
2779
- return `${innermost}::${nameNode.text}`;
2933
+ const innermost2 = outerNesting[0] ?? null;
2934
+ if (nameNode.type === "scope_resolution" && innermost2 !== null && !nameNode.text.startsWith("::")) {
2935
+ return `${innermost2}::${nameNode.text}`;
2780
2936
  }
2781
2937
  return qualifyConstantRef(nameNode, outerNesting);
2782
2938
  }
@@ -3400,24 +3556,38 @@ function isCall(node, locals, inherited) {
3400
3556
  }
3401
3557
  return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
3402
3558
  }
3403
- function collectCalls(node, locals, inherited, found) {
3559
+ function collectCalls(node, read, inherited, found) {
3404
3560
  for (const child of children2(node)) {
3405
- if (OWN_BODY_TYPES.has(child.type)) {
3561
+ if (read.stops.has(child.type)) {
3406
3562
  continue;
3407
3563
  }
3408
- if (isCall(child, locals, inherited)) {
3564
+ if (isCall(child, read.locals, inherited)) {
3409
3565
  found.push(child);
3410
3566
  }
3411
- collectCalls(child, locals, inherited, found);
3567
+ collectCalls(child, read, inherited, found);
3412
3568
  }
3413
3569
  return found;
3414
3570
  }
3415
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
3571
+ function methodBody(definitionNode) {
3416
3572
  const body = field(definitionNode, "body");
3417
3573
  if (body === null) {
3418
- return [];
3574
+ return null;
3419
3575
  }
3420
- return collectCalls(body, localNamesIn(definitionNode), inherited, []);
3576
+ return {
3577
+ body,
3578
+ locals: localNamesIn(definitionNode),
3579
+ stops: OWN_BODY_TYPES
3580
+ };
3581
+ }
3582
+ function moduleScopeBody(root) {
3583
+ return {
3584
+ body: root,
3585
+ locals: localNamesIn(root, MODULE_SCOPE_STOPS),
3586
+ stops: MODULE_SCOPE_STOPS
3587
+ };
3588
+ }
3589
+ function bodyCalls(read, inherited = NO_INHERITED_METHODS) {
3590
+ return collectCalls(read.body, read, inherited, []);
3421
3591
  }
3422
3592
  function calleeText(call) {
3423
3593
  const receiver = field(call, "receiver");
@@ -3461,14 +3631,23 @@ function enclosingStatement(call, body) {
3461
3631
  return current;
3462
3632
  }
3463
3633
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
3464
- const body = field(definitionNode, "body");
3465
- if (body === null) {
3634
+ const read = methodBody(definitionNode);
3635
+ if (read === null) {
3466
3636
  return [];
3467
3637
  }
3468
- const calls = callsReported(
3469
- bodyCalls(definitionNode, inherited),
3470
- keepsArglessCall
3638
+ return effectsOfBody(read, inherited, keepsArglessCall, facts);
3639
+ }
3640
+ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
3641
+ return effectsOfBody(
3642
+ moduleScopeBody(root),
3643
+ inherited,
3644
+ keepsArglessCall,
3645
+ facts
3471
3646
  );
3647
+ }
3648
+ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
3649
+ const body = read.body;
3650
+ const calls = callsReported(bodyCalls(read, inherited), keepsArglessCall);
3472
3651
  if (calls.length === 0) {
3473
3652
  return [];
3474
3653
  }
@@ -3874,6 +4053,57 @@ function clientCallUnits(root, pack, pattern, options) {
3874
4053
  }
3875
4054
  return units;
3876
4055
  }
4056
+ function askClientCallReads(root, patterns, options) {
4057
+ const facts = options.facts;
4058
+ if (facts === void 0 || patterns.length === 0) {
4059
+ return;
4060
+ }
4061
+ const calls = [];
4062
+ for (const method of methodDefinitions(root)) {
4063
+ calls.push(...callsUnder(method));
4064
+ }
4065
+ if (patterns.some((pattern) => (pattern.receiverBuilders ?? []).length > 0)) {
4066
+ askWrittenValues(receiversOf(calls), facts);
4067
+ }
4068
+ askWrittenValues(requestArguments(calls, patterns, options), facts);
4069
+ }
4070
+ function receiversOf(calls) {
4071
+ const found = [];
4072
+ for (const call of calls) {
4073
+ const receiver = field(call, "receiver");
4074
+ if (receiver !== null) {
4075
+ found.push(receiver);
4076
+ }
4077
+ }
4078
+ return found;
4079
+ }
4080
+ function requestArguments(calls, patterns, options) {
4081
+ const found = [];
4082
+ for (const call of calls) {
4083
+ const receiver = field(call, "receiver");
4084
+ const called = field(call, "method")?.text;
4085
+ if (receiver === null || called === void 0) {
4086
+ continue;
4087
+ }
4088
+ for (const pattern of patterns) {
4089
+ if (!isLibraryReceiver(receiver, pattern, options)) {
4090
+ continue;
4091
+ }
4092
+ const argument = requestArgument(call, called, pattern);
4093
+ if (argument !== void 0) {
4094
+ found.push(argument);
4095
+ }
4096
+ }
4097
+ }
4098
+ return found;
4099
+ }
4100
+ function requestArgument(call, called, pattern) {
4101
+ const args = readCallArgs(field(call, "arguments"));
4102
+ if (pattern.verbMethodNames[called] !== void 0) {
4103
+ return urlNodeIn(args, pattern);
4104
+ }
4105
+ return pattern.requestObject?.attribute === called ? args.positional[0] : void 0;
4106
+ }
3877
4107
  function methodDefinitions(node, found = []) {
3878
4108
  for (const child of node.namedChildren) {
3879
4109
  if (child === null) {
@@ -3998,9 +4228,11 @@ function builderCallBehind(receiver, pattern, facts) {
3998
4228
  return constant2 !== null && namesConstant(constant2, pattern.constantName) ? written : null;
3999
4229
  }
4000
4230
  function urlIn(args, pattern, options, site) {
4231
+ return pathAt(urlNodeIn(args, pattern), options, site);
4232
+ }
4233
+ function urlNodeIn(args, pattern) {
4001
4234
  const keyword = pattern.url.keyword;
4002
- const written = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
4003
- return pathAt(written, options, site);
4235
+ return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
4004
4236
  }
4005
4237
  function pathAt(written, options, site) {
4006
4238
  if (written === void 0) {
@@ -4077,19 +4309,161 @@ function returnBranch(range, effects) {
4077
4309
  var import_behavioral_ir2 = require("@suss/behavioral-ir");
4078
4310
  var import_extractor4 = require("@suss/extractor");
4079
4311
  var RUBY_ENV_RECOGNITION = "ruby-env";
4312
+ var ENV_DEFAULTED = "rbEnvDefaulted";
4080
4313
  var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
4081
- function envReadEffects(root) {
4082
- const effects = [];
4314
+ function envReadEffects(root, facts) {
4315
+ const helpers = helperFactsOf(facts);
4316
+ const slots = [];
4083
4317
  (0, import_extractor4.walkDescendants)(root, null, {
4084
4318
  at: (node) => {
4085
4319
  const read = envReadAt(node);
4086
4320
  if (read !== null) {
4087
- effects.push(configReadEffect(read));
4321
+ slots.push({ read });
4322
+ return;
4323
+ }
4324
+ if (helpers !== null && node.type === "call") {
4325
+ slots.push({ call: node });
4088
4326
  }
4089
4327
  },
4090
4328
  into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? import_extractor4.SKIP_CHILDREN : null
4091
4329
  });
4092
- return effects;
4330
+ const calls = slots.flatMap((slot) => "call" in slot ? [slot.call] : []);
4331
+ const throughHelpers = helpers === null ? new NodeMap() : helperReads(calls, helpers);
4332
+ return slots.flatMap((slot) => {
4333
+ if ("read" in slot) {
4334
+ return [configReadEffect(slot.read)];
4335
+ }
4336
+ return (throughHelpers.get(slot.call) ?? []).map(configReadEffect);
4337
+ });
4338
+ }
4339
+ function helperFactsOf(facts) {
4340
+ if (facts === void 0) {
4341
+ return null;
4342
+ }
4343
+ const named2 = namedParameters(facts.db);
4344
+ if (named2.size === 0) {
4345
+ return null;
4346
+ }
4347
+ return { ...facts, named: named2 };
4348
+ }
4349
+ var namedByDb = /* @__PURE__ */ new WeakMap();
4350
+ function namedParameters(db) {
4351
+ const memo = namedByDb.get(db);
4352
+ if (memo !== void 0) {
4353
+ return memo;
4354
+ }
4355
+ const named2 = /* @__PURE__ */ new Map();
4356
+ namedByDb.set(db, named2);
4357
+ const objects = db.facts("environmentObject").map((row) => String(row[0]));
4358
+ if (objects.length === 0) {
4359
+ return named2;
4360
+ }
4361
+ resolveEnvObjects(db, objects);
4362
+ for (const row of db.facts("wantedParamNamesEnv")) {
4363
+ const found = named2.get(String(row[0]));
4364
+ if (found === void 0) {
4365
+ named2.set(String(row[0]), [String(row[1])]);
4366
+ continue;
4367
+ }
4368
+ found.push(String(row[1]));
4369
+ }
4370
+ return named2;
4371
+ }
4372
+ function emitEnvFacts(db, file, root) {
4373
+ (0, import_extractor4.walkDescendants)(root, null, {
4374
+ at: (node) => {
4375
+ if (isEnv(node) && handsOnward(node)) {
4376
+ db.add("environmentObject", [
4377
+ readKey(file, node, enclosingDefinition(node))
4378
+ ]);
4379
+ }
4380
+ const site = envSiteAt(node);
4381
+ if (site !== null && site.defaulted && stringLiteralValue(site.name) === null) {
4382
+ db.add(ENV_DEFAULTED, [nodeId(file, site.read)]);
4383
+ }
4384
+ },
4385
+ into: () => null
4386
+ });
4387
+ }
4388
+ function handsOnward(node) {
4389
+ const parent = node.parent;
4390
+ if (parent === null) {
4391
+ return true;
4392
+ }
4393
+ if (parent.type === "element_reference" || parent.type === "call") {
4394
+ const site = envSiteAt(parent);
4395
+ return site === null || stringLiteralValue(site.name) === null;
4396
+ }
4397
+ return true;
4398
+ }
4399
+ function helperReads(calls, facts) {
4400
+ const { db, file, named: named2 } = facts;
4401
+ const callees = new NodeMap();
4402
+ for (const call of calls) {
4403
+ const enclosing = enclosingDefinition(call);
4404
+ const keys = [
4405
+ calleeKeyOf(file, call, enclosing),
4406
+ invokedKeyOf(file, call, enclosing)
4407
+ ].filter((key) => key !== null);
4408
+ if (keys.length > 0) {
4409
+ callees.set(call, keys);
4410
+ }
4411
+ }
4412
+ const reads = new NodeMap();
4413
+ if (callees.size === 0) {
4414
+ return reads;
4415
+ }
4416
+ resolveValues(
4417
+ db,
4418
+ [...callees].flatMap(([, keys]) => keys)
4419
+ );
4420
+ for (const [call, callee] of callees) {
4421
+ const args = namingArguments(db, named2, call, callee);
4422
+ if (args.length === 0) {
4423
+ continue;
4424
+ }
4425
+ const found = readsAtCall(db, call, args);
4426
+ if (found.length > 0) {
4427
+ reads.set(call, found);
4428
+ }
4429
+ }
4430
+ return reads;
4431
+ }
4432
+ function namingArguments(db, named2, call, callees) {
4433
+ const { positional, keyword } = readCallArgs(field(call, "arguments"));
4434
+ const found = [];
4435
+ for (const func of callees.flatMap((key) => resolvedFunctions(db, key))) {
4436
+ for (const row of db.lookup("paramOf", 0, func)) {
4437
+ const sites = named2.get(String(row[2]));
4438
+ const argument = positional[Number(row[1])];
4439
+ if (sites !== void 0 && argument !== void 0) {
4440
+ found.push({ sites, argument });
4441
+ }
4442
+ }
4443
+ for (const row of db.lookup("paramNamed", 0, func)) {
4444
+ const sites = named2.get(String(row[2]));
4445
+ const argument = keyword[String(row[1])];
4446
+ if (sites !== void 0 && argument !== void 0) {
4447
+ found.push({ sites, argument });
4448
+ }
4449
+ }
4450
+ }
4451
+ return found;
4452
+ }
4453
+ function readsAtCall(db, call, args) {
4454
+ const defaultedAtCall = isDefaultedAt(call);
4455
+ const byName = /* @__PURE__ */ new Map();
4456
+ for (const { sites, argument } of args) {
4457
+ const name = stringLiteralValue(argument) ?? stringValueOf(argument, db);
4458
+ if (name === null) {
4459
+ continue;
4460
+ }
4461
+ for (const site of sites) {
4462
+ const defaulted = defaultedAtCall || db.lookup(ENV_DEFAULTED, 0, site).length > 0;
4463
+ byName.set(name, (byName.get(name) ?? true) && defaulted);
4464
+ }
4465
+ }
4466
+ return [...byName].map(([name, defaulted]) => ({ name, defaulted }));
4093
4467
  }
4094
4468
  function configReadEffect(read) {
4095
4469
  return {
@@ -4108,15 +4482,23 @@ function configReadEffect(read) {
4108
4482
  };
4109
4483
  }
4110
4484
  function envReadAt(node) {
4485
+ const site = envSiteAt(node);
4486
+ if (site === null) {
4487
+ return null;
4488
+ }
4489
+ const name = stringLiteralValue(site.name);
4490
+ return name === null ? null : { name, defaulted: site.defaulted };
4491
+ }
4492
+ function envSiteAt(node) {
4111
4493
  if (node.type === "element_reference") {
4112
- return elementRead(node);
4494
+ return elementSite(node);
4113
4495
  }
4114
4496
  if (node.type === "call") {
4115
- return fetchRead(node);
4497
+ return fetchSite(node);
4116
4498
  }
4117
4499
  return null;
4118
4500
  }
4119
- function elementRead(node) {
4501
+ function elementSite(node) {
4120
4502
  const object = field(node, "object");
4121
4503
  if (object === null || !isEnv(object) || isAssignedTo(node)) {
4122
4504
  return null;
@@ -4124,24 +4506,23 @@ function elementRead(node) {
4124
4506
  const index = node.namedChildren.find(
4125
4507
  (child) => child !== null && child.id !== object.id
4126
4508
  );
4127
- const name = index === void 0 ? null : stringLiteralValue(index);
4128
- if (name === null) {
4509
+ if (index === void 0) {
4129
4510
  return null;
4130
4511
  }
4131
- return { name, defaulted: isDefaultedAt(node) };
4512
+ return { read: node, name: index, defaulted: isDefaultedAt(node) };
4132
4513
  }
4133
- function fetchRead(node) {
4514
+ function fetchSite(node) {
4134
4515
  const receiver = field(node, "receiver");
4135
4516
  if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
4136
4517
  return null;
4137
4518
  }
4138
4519
  const { positional } = readCallArgs(field(node, "arguments"));
4139
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
4140
- if (name === null) {
4520
+ const name = positional[0];
4521
+ if (name === void 0) {
4141
4522
  return null;
4142
4523
  }
4143
4524
  const hasDefault = positional.length > 1 || field(node, "block") !== null;
4144
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
4525
+ return { read: node, name, defaulted: hasDefault || isDefaultedAt(node) };
4145
4526
  }
4146
4527
  function isEnv(node) {
4147
4528
  if (node.type === "constant") {
@@ -4315,6 +4696,9 @@ function methodNamesOf(args, filter, facts) {
4315
4696
  const value = named2 === void 0 ? null : stringValueOf(named2, facts);
4316
4697
  return value === null ? [] : [value];
4317
4698
  }
4699
+ return symbolArgumentNames(args, facts);
4700
+ }
4701
+ function symbolArgumentNames(args, facts) {
4318
4702
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
4319
4703
  }
4320
4704
  function skipsOf(statement, filterName, facts) {
@@ -4353,19 +4737,315 @@ function actionsUnder(node, facts) {
4353
4737
  }
4354
4738
 
4355
4739
  // src/storage.ts
4356
- var import_ir_core = require("@suss/ir-core");
4740
+ var import_ir_core2 = require("@suss/ir-core");
4741
+ var import_resolution7 = require("@suss/resolution");
4742
+
4743
+ // src/baseClass.ts
4357
4744
  var import_resolution6 = require("@suss/resolution");
4358
- function children3(node) {
4359
- return node.namedChildren.filter((child) => child !== null);
4745
+ function classBehind(facts, file, constant2) {
4746
+ const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4747
+ const bound = new Set(
4748
+ facts.lookup("binds", 0, key).map((row) => String(row[1]))
4749
+ );
4750
+ return bound.size === 1 ? [...bound][0] : void 0;
4751
+ }
4752
+ function reachesBase(facts, classKey, bases) {
4753
+ (0, import_resolution6.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4754
+ return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4360
4755
  }
4756
+ function enclosingClassKey(node, file) {
4757
+ let current = node.parent;
4758
+ while (current !== null) {
4759
+ if (NESTING_TYPES.has(current.type)) {
4760
+ return nodeId(file, current);
4761
+ }
4762
+ current = current.parent;
4763
+ }
4764
+ return null;
4765
+ }
4766
+
4767
+ // src/loaders.ts
4361
4768
  function receiverOf(node) {
4362
4769
  return node.type === "call" ? field(node, "receiver") : null;
4363
4770
  }
4364
4771
  function methodOf(node) {
4365
4772
  return field(node, "method")?.text ?? "";
4366
4773
  }
4774
+ function isLoader(node, loader) {
4775
+ if (node === null) {
4776
+ return false;
4777
+ }
4778
+ if (node.type === "identifier") {
4779
+ return node.text === loader.loader;
4780
+ }
4781
+ return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
4782
+ }
4783
+ function loaderPick(call, loader) {
4784
+ const method = methodOf(call);
4785
+ const receiver = receiverOf(call);
4786
+ if (receiver === null) {
4787
+ return loader.shortcuts.includes(method) ? call : null;
4788
+ }
4789
+ if (!loader.reads.includes(method) || receiver.type !== "call") {
4790
+ return null;
4791
+ }
4792
+ const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
4793
+ return picks ? receiver : null;
4794
+ }
4795
+ function pickedSource(call, loader) {
4796
+ const source = loader.source;
4797
+ const pick = source === void 0 ? null : loaderPick(call, loader);
4798
+ if (source === void 0 || pick === null || methodOf(pick) !== loader.pick) {
4799
+ return null;
4800
+ }
4801
+ const args = field(pick, "arguments");
4802
+ const given = args === null ? void 0 : args.namedChildren[source.at];
4803
+ return given?.type === "constant" || given?.type === "scope_resolution" ? given : null;
4804
+ }
4805
+
4806
+ // src/rawSql.ts
4807
+ var import_ir_core = require("@suss/ir-core");
4808
+ var import_sql = require("@suss/sql");
4809
+ var import_values12 = require("@suss/values");
4810
+ var MOST_HOPS = 8;
4811
+ var NOWHERE = { scope: null, container: null };
4812
+ function rawSqlEffects(call, options) {
4813
+ for (const pattern of options.patterns) {
4814
+ const effects = patternEffects(call, pattern, options);
4815
+ if (effects.length > 0) {
4816
+ return effects;
4817
+ }
4818
+ }
4819
+ return [];
4820
+ }
4821
+ function patternEffects(call, pattern, options) {
4822
+ const receiver = field(call, "receiver");
4823
+ if (receiver === null) {
4824
+ return [];
4825
+ }
4826
+ const address = addressBehind(receiver, pattern, options, 0);
4827
+ if (address === null) {
4828
+ return [];
4829
+ }
4830
+ const method = field(call, "method")?.text ?? "";
4831
+ const args = readCallArgs(field(call, "arguments"));
4832
+ const place = pattern.statements?.[method];
4833
+ if (place !== void 0) {
4834
+ const statement = statementAt(
4835
+ args,
4836
+ place,
4837
+ options.facts,
4838
+ pattern.bindPlaceholder
4839
+ );
4840
+ return statementEffects(call, statement, address, pattern);
4841
+ }
4842
+ return rowCallEffects(call, args, address, pattern, options);
4843
+ }
4844
+ function statementEffects(call, statement, address, store) {
4845
+ if (statement === null) {
4846
+ return [];
4847
+ }
4848
+ return (0, import_sql.readSqlAccess)(statement, { dialect: store.dialect }).map(
4849
+ (access) => effectOf(
4850
+ call,
4851
+ store,
4852
+ {
4853
+ scope: innermost(access.qualifier) ?? address.scope,
4854
+ container: access.table
4855
+ },
4856
+ {
4857
+ kind: access.kind,
4858
+ fields: access.fields,
4859
+ selector: access.selector
4860
+ }
4861
+ )
4862
+ );
4863
+ }
4864
+ function innermost(qualifier) {
4865
+ return qualifier[qualifier.length - 1] ?? null;
4866
+ }
4867
+ function rowCallEffects(call, args, address, pattern, options) {
4868
+ const method = field(call, "method")?.text ?? "";
4869
+ const rowCall = pattern.rowCalls?.[method];
4870
+ if (rowCall === void 0) {
4871
+ return [];
4872
+ }
4873
+ const given = rowCall.container === void 0 ? null : namedTable(stringAt(args, rowCall.container, options.facts));
4874
+ const reached = {
4875
+ scope: given?.scope ?? address.scope,
4876
+ container: given?.container ?? address.container
4877
+ };
4878
+ if (reached.container === null) {
4879
+ return [];
4880
+ }
4881
+ return [
4882
+ effectOf(call, pattern, reached, {
4883
+ kind: rowCall.kind,
4884
+ fields: [],
4885
+ selector: []
4886
+ })
4887
+ ];
4888
+ }
4889
+ function namedTable(name) {
4890
+ const split = name === null ? null : (0, import_sql.splitQualifiedTable)(name);
4891
+ return split === null ? null : { scope: innermost(split.qualifier), container: split.table };
4892
+ }
4893
+ function effectOf(call, store, address, access) {
4894
+ const operation = field(call, "method")?.text ?? "";
4895
+ return {
4896
+ type: "interaction",
4897
+ binding: (0, import_ir_core.storageBinding)({
4898
+ recognition: "ruby-raw-sql",
4899
+ storageSystem: store.storageSystem,
4900
+ scope: address.scope ?? store.scope ?? "default",
4901
+ container: address.container
4902
+ }),
4903
+ callee: call.text,
4904
+ interaction: {
4905
+ class: "storage-access",
4906
+ kind: access.kind,
4907
+ fields: access.fields,
4908
+ operation,
4909
+ ...access.selector.length > 0 ? { selector: access.selector } : {}
4910
+ }
4911
+ };
4912
+ }
4913
+ function addressBehind(receiver, pattern, options, hops) {
4914
+ if (hops > MOST_HOPS) {
4915
+ return null;
4916
+ }
4917
+ if (receiver.type !== "call") {
4918
+ const written = writtenNodeOf(receiver, options.facts);
4919
+ if (written !== null && written.id !== receiver.id) {
4920
+ return addressBehind(written, pattern, options, hops + 1);
4921
+ }
4922
+ return receiver.type === "identifier" && buildsOnOwnClass(receiver, receiver.text, pattern, options) ? NOWHERE : null;
4923
+ }
4924
+ const method = field(receiver, "method")?.text ?? "";
4925
+ const inner = field(receiver, "receiver");
4926
+ if (inner === null) {
4927
+ return buildsOnOwnClass(receiver, method, pattern, options) ? NOWHERE : null;
4928
+ }
4929
+ if (namesConstant2(inner, pattern.constantName) || namesSubclass(inner, pattern, options)) {
4930
+ return pattern.clientBuilders.includes(method) ? NOWHERE : null;
4931
+ }
4932
+ const addressing = pattern.addressing?.[method];
4933
+ if (addressing === void 0 || inner === null) {
4934
+ return null;
4935
+ }
4936
+ const behind = addressBehind(inner, pattern, options, hops + 1);
4937
+ if (behind === null) {
4938
+ return null;
4939
+ }
4940
+ const named2 = stringAt(
4941
+ readCallArgs(field(receiver, "arguments")),
4942
+ addressing,
4943
+ options.facts
4944
+ );
4945
+ if (addressing.says === "scope") {
4946
+ return { scope: named2 ?? behind.scope, container: behind.container };
4947
+ }
4948
+ const table = namedTable(named2);
4949
+ return {
4950
+ scope: table?.scope ?? behind.scope,
4951
+ container: table?.container ?? behind.container
4952
+ };
4953
+ }
4954
+ function namesConstant2(receiver, constantName2) {
4955
+ if (receiver.type === "constant") {
4956
+ return receiver.text === constantName2;
4957
+ }
4958
+ return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
4959
+ }
4960
+ function namesSubclass(receiver, pattern, options) {
4961
+ const bases = pattern.baseClasses ?? [];
4962
+ const facts = options.facts;
4963
+ const file = options.file;
4964
+ if (bases.length === 0 || facts === void 0 || file === void 0 || receiver.type !== "constant" && receiver.type !== "scope_resolution") {
4965
+ return false;
4966
+ }
4967
+ const classKey = classBehind(facts, file, receiver);
4968
+ return classKey !== void 0 && reachesBase(facts, classKey, bases);
4969
+ }
4970
+ function buildsOnOwnClass(node, method, pattern, options) {
4971
+ const bases = pattern.baseClasses ?? [];
4972
+ const facts = options.facts;
4973
+ const file = options.file;
4974
+ if (bases.length === 0 || facts === void 0 || file === void 0 || !pattern.clientBuilders.includes(method)) {
4975
+ return false;
4976
+ }
4977
+ const classKey = enclosingClassKey(node, file);
4978
+ return classKey !== null && reachesBase(facts, classKey, bases);
4979
+ }
4980
+ function argumentAt(args, place) {
4981
+ const keyword = place.keyword;
4982
+ return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[place.at];
4983
+ }
4984
+ function stringAt(args, place, facts) {
4985
+ const written = argumentAt(args, place);
4986
+ if (written === void 0) {
4987
+ return null;
4988
+ }
4989
+ const pieces = (0, import_values12.piecesOf)(evaluatedValue(written, facts));
4990
+ const only = pieces.length === 1 ? pieces[0] : void 0;
4991
+ if (only === void 0 || only.kind !== "text" || only.options.length !== 1) {
4992
+ return null;
4993
+ }
4994
+ return only.options[0] ?? null;
4995
+ }
4996
+ function statementAt(args, place, facts, bindPlaceholder) {
4997
+ const written = argumentAt(args, place);
4998
+ if (written === void 0) {
4999
+ return null;
5000
+ }
5001
+ const parts = literalParts(statementValue(evaluatedValue(written, facts)));
5002
+ if (parts === null) {
5003
+ return null;
5004
+ }
5005
+ const statement = (0, import_sql.sqlFromParts)(bindsSplit(parts, bindPlaceholder));
5006
+ return statement.trim() === "" ? null : statement;
5007
+ }
5008
+ function bindsSplit(parts, bindPlaceholder) {
5009
+ if (bindPlaceholder === void 0 || bindPlaceholder === "") {
5010
+ return [...parts];
5011
+ }
5012
+ return parts.flatMap((part) => part.split(bindPlaceholder));
5013
+ }
5014
+ function statementValue(value) {
5015
+ if (value.kind !== "sequence") {
5016
+ return value;
5017
+ }
5018
+ const first = value.items[0];
5019
+ return first === void 0 ? value : first.value;
5020
+ }
5021
+ function literalParts(value) {
5022
+ if (value.kind !== "string") {
5023
+ return null;
5024
+ }
5025
+ const parts = [""];
5026
+ for (const piece of value.pieces) {
5027
+ const only = piece.kind === "text" ? piece.options : [];
5028
+ if (only.length === 1) {
5029
+ parts[parts.length - 1] += only[0] ?? "";
5030
+ continue;
5031
+ }
5032
+ parts.push("");
5033
+ }
5034
+ return parts;
5035
+ }
5036
+
5037
+ // src/storage.ts
5038
+ function children3(node) {
5039
+ return node.namedChildren.filter((child) => child !== null);
5040
+ }
5041
+ function receiverOf2(node) {
5042
+ return node.type === "call" ? field(node, "receiver") : null;
5043
+ }
5044
+ function methodOf2(node) {
5045
+ return field(node, "method")?.text ?? "";
5046
+ }
4367
5047
  function rootConstant(node) {
4368
- const receiver = receiverOf(node);
5048
+ const receiver = receiverOf2(node);
4369
5049
  if (receiver === null) {
4370
5050
  return null;
4371
5051
  }
@@ -4380,17 +5060,6 @@ function isConstant(node) {
4380
5060
  function constantName(constant2) {
4381
5061
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
4382
5062
  }
4383
- function reachesBase(facts, classKey, bases) {
4384
- (0, import_resolution6.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4385
- return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4386
- }
4387
- function classBehind(facts, file, constant2) {
4388
- const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4389
- const bound = new Set(
4390
- facts.lookup("binds", 0, key).map((row) => String(row[1]))
4391
- );
4392
- return bound.size === 1 ? [...bound][0] : void 0;
4393
- }
4394
5063
  function modelPattern(constant2, file, options) {
4395
5064
  const classKey = classBehind(options.facts, file, constant2);
4396
5065
  if (classKey === void 0) {
@@ -4405,7 +5074,7 @@ function chainLinks(call) {
4405
5074
  let current = call;
4406
5075
  while (current !== null && current.type === "call") {
4407
5076
  links.push(current);
4408
- current = receiverOf(current);
5077
+ current = receiverOf2(current);
4409
5078
  }
4410
5079
  return links;
4411
5080
  }
@@ -4418,9 +5087,17 @@ function kindOfCall(pattern, method) {
4418
5087
  }
4419
5088
  return void 0;
4420
5089
  }
5090
+ function libraryCallOf(pattern, method) {
5091
+ const place = pattern.statements?.[method];
5092
+ if (place !== void 0) {
5093
+ return { how: "statement", place };
5094
+ }
5095
+ const kind = kindOfCall(pattern, method);
5096
+ return kind === void 0 ? void 0 : { how: "rows", kind };
5097
+ }
4421
5098
  function someLibraryDefines(options, method) {
4422
5099
  return options.patterns.some(
4423
- (pattern) => kindOfCall(pattern, method) !== void 0
5100
+ (pattern) => libraryCallOf(pattern, method) !== void 0
4424
5101
  );
4425
5102
  }
4426
5103
  function argumentsOf2(call) {
@@ -4455,7 +5132,7 @@ function hasPositionalArgument(call, facts) {
4455
5132
  );
4456
5133
  }
4457
5134
  function columnsAskedFor(call, pattern, facts) {
4458
- if (!(pattern.columnArguments ?? []).includes(methodOf(call))) {
5135
+ if (!(pattern.columnArguments ?? []).includes(methodOf2(call))) {
4459
5136
  return [];
4460
5137
  }
4461
5138
  return argumentsOf2(call).map((argument) => stringValueOf(argument, facts)).filter((column) => column !== null);
@@ -4465,14 +5142,14 @@ function primaryKeySelector(call, pattern, facts) {
4465
5142
  if (byPrimaryKey === void 0) {
4466
5143
  return [];
4467
5144
  }
4468
- const picks = byPrimaryKey.methods.includes(methodOf(call)) && hasPositionalArgument(call, facts);
5145
+ const picks = byPrimaryKey.methods.includes(methodOf2(call)) && hasPositionalArgument(call, facts);
4469
5146
  return picks ? [byPrimaryKey.column] : [];
4470
5147
  }
4471
5148
  function selectorOf(call, pattern, facts) {
4472
5149
  const picked = [];
4473
5150
  for (const link of chainLinks(call)) {
4474
5151
  picked.push(...primaryKeySelector(link, pattern, facts));
4475
- if (pattern.reads.includes(methodOf(link))) {
5152
+ if (pattern.reads.includes(methodOf2(link))) {
4476
5153
  picked.push(...keywordKeys(link, facts));
4477
5154
  }
4478
5155
  }
@@ -4489,10 +5166,10 @@ function fieldsOf(call, pattern, kind, facts) {
4489
5166
  ];
4490
5167
  }
4491
5168
  function storageEffect(call, container, pattern, kind, selector, fields) {
4492
- const operation = methodOf(call);
5169
+ const operation = methodOf2(call);
4493
5170
  return {
4494
5171
  type: "interaction",
4495
- binding: (0, import_ir_core.storageBinding)({
5172
+ binding: (0, import_ir_core2.storageBinding)({
4496
5173
  recognition: "ruby-storage",
4497
5174
  storageSystem: pattern.storageSystem,
4498
5175
  scope: "default",
@@ -4509,7 +5186,7 @@ function storageEffect(call, container, pattern, kind, selector, fields) {
4509
5186
  };
4510
5187
  }
4511
5188
  function classSettledOn(facts, key) {
4512
- (0, import_resolution6.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
5189
+ (0, import_resolution7.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4513
5190
  const objects = new Set(
4514
5191
  facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
4515
5192
  );
@@ -4541,7 +5218,7 @@ function receiverClass(call, file, options, enclosing) {
4541
5218
  if (constant2 !== null) {
4542
5219
  return constantReceiver(constant2, file, options.facts);
4543
5220
  }
4544
- const receiver = receiverOf(call);
5221
+ const receiver = receiverOf2(call);
4545
5222
  if (receiver === null) {
4546
5223
  return void 0;
4547
5224
  }
@@ -4549,60 +5226,91 @@ function receiverClass(call, file, options, enclosing) {
4549
5226
  }
4550
5227
  function libraryCallIn(call, options) {
4551
5228
  return chainLinks(call).find(
4552
- (link) => someLibraryDefines(options, methodOf(link))
5229
+ (link) => someLibraryDefines(options, methodOf2(link))
4553
5230
  ) ?? null;
4554
5231
  }
5232
+ function modelStatementEffects(call, place, pattern, options) {
5233
+ const args = readCallArgs(field(call, "arguments"));
5234
+ return statementEffects(
5235
+ call,
5236
+ statementAt(args, place, options.facts, pattern.bindPlaceholder),
5237
+ NOWHERE,
5238
+ { storageSystem: pattern.storageSystem, dialect: pattern.storageSystem }
5239
+ );
5240
+ }
5241
+ function callbacksRunBy(facts, classKey, pattern, writeMethod) {
5242
+ const events = pattern.callbacks?.eventOf[writeMethod] ?? [];
5243
+ if (events.length === 0) {
5244
+ return [];
5245
+ }
5246
+ (0, import_resolution7.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
5247
+ const found = /* @__PURE__ */ new Map();
5248
+ for (const row of facts.lookup("wantedCallbackMethod", 0, classKey)) {
5249
+ if (events.includes(String(row[1]))) {
5250
+ const key = String(row[3]);
5251
+ found.set(key, { name: String(row[2]), key });
5252
+ }
5253
+ }
5254
+ return [...found.values()];
5255
+ }
5256
+ function callbackEffect(callback) {
5257
+ return {
5258
+ type: "invocation",
5259
+ callee: callback.name,
5260
+ args: [],
5261
+ async: false
5262
+ };
5263
+ }
4555
5264
  function modelCallEffects(call, file, options, enclosing) {
4556
5265
  const worked = libraryCallIn(call, options);
4557
5266
  if (worked === null) {
4558
5267
  return [];
4559
5268
  }
4560
- const method = methodOf(worked);
5269
+ const method = methodOf2(worked);
4561
5270
  const target = receiverClass(worked, file, options, enclosing);
4562
5271
  if (target === void 0) {
4563
5272
  return [];
4564
5273
  }
4565
5274
  for (const pattern of options.patterns) {
4566
- const kind = kindOfCall(pattern, method);
4567
- if (kind === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
5275
+ const library = libraryCallOf(pattern, method);
5276
+ if (library === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
4568
5277
  continue;
4569
5278
  }
4570
5279
  if (projectDeclares(options.facts, target.classKey, method)) {
4571
5280
  return [];
4572
5281
  }
5282
+ if (library.how === "statement") {
5283
+ return modelStatementEffects(worked, library.place, pattern, options);
5284
+ }
5285
+ const ran = library.kind === "write" ? callbacksRunBy(options.facts, target.classKey, pattern, method) : [];
4573
5286
  return [
4574
5287
  storageEffect(
4575
5288
  worked,
4576
5289
  target.container,
4577
5290
  pattern,
4578
- kind,
5291
+ library.kind,
4579
5292
  selectorOf(worked, pattern, options.facts),
4580
- fieldsOf(worked, pattern, kind, options.facts)
4581
- )
5293
+ fieldsOf(worked, pattern, library.kind, options.facts)
5294
+ ),
5295
+ ...ran.map(callbackEffect)
4582
5296
  ];
4583
5297
  }
4584
5298
  return [];
4585
5299
  }
4586
- function isLoader(node, loader) {
4587
- if (node === null) {
4588
- return false;
4589
- }
4590
- if (node.type === "identifier") {
4591
- return node.text === loader.loader;
4592
- }
4593
- return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
4594
- }
4595
- function loaderPick(call, loader) {
4596
- const method = methodOf(call);
4597
- const receiver = receiverOf(call);
4598
- if (receiver === null) {
4599
- return loader.shortcuts.includes(method) ? call : null;
5300
+ function callbacksReached(call, file, options, enclosing = null) {
5301
+ const worked = libraryCallIn(call, options);
5302
+ const target = worked === null ? void 0 : receiverClass(worked, file, options, enclosing);
5303
+ if (worked === null || target === void 0) {
5304
+ return [];
4600
5305
  }
4601
- if (!loader.reads.includes(method) || receiver.type !== "call") {
4602
- return null;
5306
+ const method = methodOf2(worked);
5307
+ for (const pattern of options.patterns) {
5308
+ if (libraryCallOf(pattern, method)?.how !== "rows" || kindOfCall(pattern, method) !== "write" || !reachesBase(options.facts, target.classKey, pattern.baseClasses) || projectDeclares(options.facts, target.classKey, method)) {
5309
+ continue;
5310
+ }
5311
+ return callbacksRunBy(options.facts, target.classKey, pattern, method);
4603
5312
  }
4604
- const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
4605
- return picks ? receiver : null;
5313
+ return [];
4606
5314
  }
4607
5315
  function loaderCallEffects(call, file, options) {
4608
5316
  const effects = [];
@@ -4628,10 +5336,24 @@ function loaderCallEffects(call, file, options) {
4628
5336
  }
4629
5337
  function effectsOfCall(call, file, options, enclosing) {
4630
5338
  const onModel = modelCallEffects(call, file, options, enclosing);
4631
- return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
5339
+ if (onModel.length > 0) {
5340
+ return onModel;
5341
+ }
5342
+ const throughLoader = loaderCallEffects(call, file, options);
5343
+ if (throughLoader.length > 0) {
5344
+ return throughLoader;
5345
+ }
5346
+ return rawSqlEffects(call, {
5347
+ facts: options.facts,
5348
+ patterns: options.rawSql ?? [],
5349
+ file
5350
+ });
4632
5351
  }
4633
5352
  function storageClaims(call, file, options, enclosing = null) {
4634
- return options.patterns.length > 0 && effectsOfCall(call, file, options, enclosing).length > 0;
5353
+ return saysAnything(options) && effectsOfCall(call, file, options, enclosing).length > 0;
5354
+ }
5355
+ function saysAnything(options) {
5356
+ return options.patterns.length > 0 || (options.rawSql ?? []).length > 0;
4635
5357
  }
4636
5358
  function receiverKeysToAsk(chains, file, options, enclosing) {
4637
5359
  const keys = [];
@@ -4640,7 +5362,7 @@ function receiverKeysToAsk(chains, file, options, enclosing) {
4640
5362
  if (worked === null || rootConstant(worked) !== null) {
4641
5363
  continue;
4642
5364
  }
4643
- const receiver = receiverOf(worked);
5365
+ const receiver = receiverOf2(worked);
4644
5366
  if (receiver !== null) {
4645
5367
  keys.push(readKey(file, receiver, enclosing));
4646
5368
  }
@@ -4648,18 +5370,18 @@ function receiverKeysToAsk(chains, file, options, enclosing) {
4648
5370
  return keys;
4649
5371
  }
4650
5372
  function storageEffects(calls, file, options, enclosing = null) {
4651
- if (options.patterns.length === 0) {
5373
+ if (!saysAnything(options)) {
4652
5374
  return [];
4653
5375
  }
4654
5376
  const partOfOne = /* @__PURE__ */ new Set();
4655
5377
  for (const call of calls) {
4656
- const receiver = receiverOf(call);
5378
+ const receiver = receiverOf2(call);
4657
5379
  if (receiver !== null) {
4658
5380
  partOfOne.add(receiver.id);
4659
5381
  }
4660
5382
  }
4661
5383
  const chains = calls.filter((call) => !partOfOne.has(call.id));
4662
- (0, import_resolution6.askResolution)(
5384
+ (0, import_resolution7.askResolution)(
4663
5385
  options.facts,
4664
5386
  receiverKeysToAsk(chains, file, options, enclosing),
4665
5387
  "wanted",
@@ -4784,14 +5506,18 @@ async function discoverUnits(root, options) {
4784
5506
  file: options.absoluteFile ?? options.filePath
4785
5507
  }));
4786
5508
  const units = [];
5509
+ const clientOptions = {
5510
+ filePath: options.filePath,
5511
+ ...options.facts === void 0 ? {} : { facts: options.facts }
5512
+ };
5513
+ askClientCallReads(
5514
+ root,
5515
+ options.packs.flatMap((pack) => pack.clients ?? []),
5516
+ clientOptions
5517
+ );
4787
5518
  for (const pack of options.packs) {
4788
5519
  for (const pattern of pack.clients ?? []) {
4789
- units.push(
4790
- ...clientCallUnits(root, pack, pattern, {
4791
- filePath: options.filePath,
4792
- ...options.facts === void 0 ? {} : { facts: options.facts }
4793
- })
4794
- );
5520
+ units.push(...clientCallUnits(root, pack, pattern, clientOptions));
4795
5521
  }
4796
5522
  }
4797
5523
  for (const info of classes) {
@@ -5124,8 +5850,12 @@ function bodyOfMethod(method, file, bodyRead = {}) {
5124
5850
  bodyRead.facts
5125
5851
  );
5126
5852
  const storage = bodyRead.storage;
5853
+ const facts = bodyRead.facts;
5127
5854
  const extra = [
5128
- ...envReadEffects(method),
5855
+ ...envReadEffects(
5856
+ method,
5857
+ facts === void 0 ? void 0 : { db: facts, file }
5858
+ ),
5129
5859
  ...storage === void 0 ? [] : storageEffects(callsUnder2(method), file, storage, method)
5130
5860
  ];
5131
5861
  return {
@@ -5797,7 +6527,7 @@ function emitConstantBindings(db, perFile) {
5797
6527
  var import_node_fs4 = __toESM(require("fs"), 1);
5798
6528
  var import_node_path5 = __toESM(require("path"), 1);
5799
6529
  var import_datalog4 = require("@suss/datalog");
5800
- var import_resolution9 = require("@suss/resolution");
6530
+ var import_resolution10 = require("@suss/resolution");
5801
6531
 
5802
6532
  // src/parser.ts
5803
6533
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -5861,7 +6591,7 @@ var import_node_path4 = __toESM(require("path"), 1);
5861
6591
  var import_behavioral_ir5 = require("@suss/behavioral-ir");
5862
6592
  var import_datalog3 = require("@suss/datalog");
5863
6593
  var import_extractor10 = require("@suss/extractor");
5864
- var import_resolution8 = require("@suss/resolution");
6594
+ var import_resolution9 = require("@suss/resolution");
5865
6595
 
5866
6596
  // src/diagnostics.ts
5867
6597
  var import_extractor6 = require("@suss/extractor");
@@ -5878,6 +6608,47 @@ function buildRubyExtractionReport(args) {
5878
6608
  });
5879
6609
  }
5880
6610
 
6611
+ // src/facts/callbacks.ts
6612
+ function callbacksIn(patterns) {
6613
+ return patterns.flatMap(
6614
+ (pattern) => pattern.callbacks === void 0 ? [] : [pattern.callbacks]
6615
+ );
6616
+ }
6617
+ function emitClassCallbacks(db, file, classNode, declared) {
6618
+ const body = field(classNode, "body");
6619
+ if (body === null || declared.length === 0) {
6620
+ return;
6621
+ }
6622
+ const classKey = nodeId(file, classNode);
6623
+ for (const statement of runStatements(body)) {
6624
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
6625
+ continue;
6626
+ }
6627
+ const called = field(statement, "method")?.text ?? "";
6628
+ const args = readCallArgs(field(statement, "arguments"));
6629
+ for (const callbacks of declared) {
6630
+ const events = eventsOf(args, called, callbacks, db);
6631
+ for (const event of events) {
6632
+ for (const method of symbolArgumentNames(args, db)) {
6633
+ db.add("classCallback", [classKey, event, method]);
6634
+ }
6635
+ }
6636
+ }
6637
+ }
6638
+ }
6639
+ function eventsOf(args, called, callbacks, db) {
6640
+ const covered = callbacks.registeredBy[called];
6641
+ if (covered === void 0) {
6642
+ return [];
6643
+ }
6644
+ const written = args.keyword[callbacks.eventKeyword];
6645
+ const narrowed = written === void 0 ? null : stringValueOf(written, db);
6646
+ if (narrowed === null) {
6647
+ return covered;
6648
+ }
6649
+ return covered.filter((event) => event === narrowed);
6650
+ }
6651
+
5881
6652
  // src/facts.ts
5882
6653
  var import_node_path3 = __toESM(require("path"), 1);
5883
6654
  var import_extractor7 = require("@suss/extractor");
@@ -5948,7 +6719,7 @@ var import_datalog2 = require("@suss/datalog");
5948
6719
  var import_extractor8 = require("@suss/extractor");
5949
6720
 
5950
6721
  // src/reach/resolveCallee.ts
5951
- var import_resolution7 = require("@suss/resolution");
6722
+ var import_resolution8 = require("@suss/resolution");
5952
6723
  var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
5953
6724
  var INVOKES_RECEIVER = "call";
5954
6725
  var STOP_FOR = {
@@ -5977,10 +6748,14 @@ function calleeSpellings(calls, ctx) {
5977
6748
  }
5978
6749
  return {
5979
6750
  spellingOf,
5980
- outcomes: (0, import_resolution7.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
6751
+ outcomes: (0, import_resolution8.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
5981
6752
  };
5982
6753
  }
5983
6754
  function resolveCallee(call, site, ctx, read) {
6755
+ const throughLoader = loaderSourceCallee(call, site, ctx);
6756
+ if (throughLoader !== null) {
6757
+ return throughLoader;
6758
+ }
5984
6759
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
5985
6760
  if (spelling.kind === "stopped") {
5986
6761
  return spelling;
@@ -5990,12 +6765,27 @@ function resolveCallee(call, site, ctx, read) {
5990
6765
  }
5991
6766
  return asCallee(spelling, outcomeFor(spelling.key, ctx, read), site, ctx);
5992
6767
  }
6768
+ function loaderSourceCallee(call, site, ctx) {
6769
+ for (const loader of ctx.loaders) {
6770
+ const constant2 = pickedSource(call, loader);
6771
+ const source = loader.source;
6772
+ if (constant2 === null || source === void 0) {
6773
+ continue;
6774
+ }
6775
+ const classKey = classBehind(ctx.facts, site.file, constant2);
6776
+ const qualifiedName = classKey === void 0 ? void 0 : ctx.classNames.get(classKey);
6777
+ if (qualifiedName !== void 0) {
6778
+ return methodOnAncestryOf(qualifiedName, source.method, ctx);
6779
+ }
6780
+ }
6781
+ return null;
6782
+ }
5993
6783
  function outcomeFor(key, ctx, read) {
5994
- return read?.outcomes.get(key) ?? (0, import_resolution7.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
6784
+ return read?.outcomes.get(key) ?? (0, import_resolution8.calleeOutcomeOf)(ctx.facts, key, RUBY_PROGRAM);
5995
6785
  }
5996
6786
  function mightReadAsACall(call, site, ctx) {
5997
6787
  const spelling = spellingFor(call, site);
5998
- return spelling.kind === "receiver" && (0, import_resolution7.couldBeSettled)(ctx.facts, spelling.key);
6788
+ return spelling.kind === "receiver" && (0, import_resolution8.couldBeSettled)(ctx.facts, spelling.key);
5999
6789
  }
6000
6790
  function readsAsACall(call, site, ctx, read) {
6001
6791
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
@@ -6171,6 +6961,7 @@ async function reachedFunctions(seeds, options) {
6171
6961
  const parameterCallsByKey = /* @__PURE__ */ new Map();
6172
6962
  const propertyReadsByKey = /* @__PURE__ */ new Map();
6173
6963
  const passedPositions = /* @__PURE__ */ new Set();
6964
+ const followedKeys = /* @__PURE__ */ new Set();
6174
6965
  for (const seed of seeds) {
6175
6966
  seedKeys.add(seed.key);
6176
6967
  functionByKey.set(seed.key, {
@@ -6218,6 +7009,9 @@ async function reachedFunctions(seeds, options) {
6218
7009
  for (const position of scan.passedPositions) {
6219
7010
  passedPositions.add(position);
6220
7011
  }
7012
+ if (scan.followed.length > 0) {
7013
+ followedKeys.add(key);
7014
+ }
6221
7015
  for (const target of scan.followed) {
6222
7016
  const calleeKey = keyOf(target);
6223
7017
  if (!functionByKey.has(calleeKey)) {
@@ -6254,7 +7048,8 @@ async function reachedFunctions(seeds, options) {
6254
7048
  argTargetsByKey,
6255
7049
  parameterCallsByKey,
6256
7050
  propertyReadsByKey,
6257
- passedPositions
7051
+ passedPositions,
7052
+ followedKeys
6258
7053
  };
6259
7054
  }
6260
7055
  function keyOf(target) {
@@ -6293,13 +7088,9 @@ function methodReferenceSymbol(node) {
6293
7088
  return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
6294
7089
  }
6295
7090
  function bodyOf2(source, options) {
6296
- const site = {
6297
- file: source.file,
6298
- method: source.node,
6299
- owner: keyOf(source),
6300
- enclosingQualifiedName: source.enclosingQualifiedName
6301
- };
6302
- const written = field(source.node, "body") === null ? [] : bodyCalls(source.node, options.inheritedMethods);
7091
+ const site = siteOf2(source);
7092
+ const read = readableBodyOf(source.node);
7093
+ const written = read === null ? [] : bodyCalls(read, options.inheritedMethods);
6303
7094
  const calls = callsReported(
6304
7095
  written,
6305
7096
  (call) => mightReadAsACall(call, site, options.context)
@@ -6311,6 +7102,17 @@ function bodyOf2(source, options) {
6311
7102
  written: calls.map((call) => ({ call, site }))
6312
7103
  };
6313
7104
  }
7105
+ function readableBodyOf(node) {
7106
+ return node.type === PROGRAM_TYPE ? moduleScopeBody(node) : methodBody(node);
7107
+ }
7108
+ function siteOf2(source) {
7109
+ return {
7110
+ file: source.file,
7111
+ method: source.node.type === PROGRAM_TYPE ? null : source.node,
7112
+ owner: keyOf(source),
7113
+ enclosingQualifiedName: source.enclosingQualifiedName
7114
+ };
7115
+ }
6314
7116
  function callsMade(asked, site, ctx, spellings) {
6315
7117
  return asked.filter(
6316
7118
  (call) => !isArglessReceiverCall(call) || readsAsACall(call, site, ctx, spellings)
@@ -6318,7 +7120,7 @@ function callsMade(asked, site, ctx, spellings) {
6318
7120
  }
6319
7121
  function scanBody(source, ctx, options, read) {
6320
7122
  const displayPathOf = options.displayPathOf;
6321
- if (field(source.node, "body") === null) {
7123
+ if (readableBodyOf(source.node) === null) {
6322
7124
  return EMPTY_SCAN;
6323
7125
  }
6324
7126
  const site = read.site;
@@ -6361,9 +7163,34 @@ function scanBody(source, ctx, options, read) {
6361
7163
  }
6362
7164
  }
6363
7165
  };
7166
+ const followCallback = (name, key) => {
7167
+ const target = ctx.definitions.get(key);
7168
+ if (target === void 0) {
7169
+ return;
7170
+ }
7171
+ const targetKey = keyOf(target);
7172
+ if (!seen.has(targetKey)) {
7173
+ seen.add(targetKey);
7174
+ followed2.push(target);
7175
+ }
7176
+ placements.place(name, {
7177
+ file: displayPathOf(target.file),
7178
+ span: spanOf(target.node)
7179
+ });
7180
+ };
6364
7181
  for (const call of calls) {
6365
7182
  const callee = calleeText(call);
6366
7183
  const outcome = resolveCallee(call, site, ctx, read.spellings);
7184
+ if (options.storage !== void 0) {
7185
+ for (const callback of callbacksReached(
7186
+ call,
7187
+ source.file,
7188
+ options.storage,
7189
+ site.method
7190
+ )) {
7191
+ followCallback(callback.name, callback.key);
7192
+ }
7193
+ }
6367
7194
  const placed = outcome.kind === "followed" ? {
6368
7195
  file: displayPathOf(outcome.target.file),
6369
7196
  span: spanOf(outcome.target.node)
@@ -6495,8 +7322,8 @@ function parameterName(param) {
6495
7322
  }
6496
7323
 
6497
7324
  // src/reach/context.ts
6498
- var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
6499
- async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map()) {
7325
+ var METHOD_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
7326
+ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map(), loaders = []) {
6500
7327
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
6501
7328
  const classes = [];
6502
7329
  const classNames = /* @__PURE__ */ new Map();
@@ -6506,7 +7333,7 @@ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dyna
6506
7333
  classes.push({ file, info });
6507
7334
  classNames.set(nodeId(file, info.node), info.qualifiedName);
6508
7335
  for (const statement of namedChildren(info.bodyNode)) {
6509
- if (METHOD_TYPES4.has(statement.type)) {
7336
+ if (METHOD_TYPES3.has(statement.type)) {
6510
7337
  classOfMethod.set(nodeId(file, statement), info.qualifiedName);
6511
7338
  }
6512
7339
  }
@@ -6572,7 +7399,8 @@ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dyna
6572
7399
  classNames,
6573
7400
  definitions,
6574
7401
  bodyBlocks,
6575
- dynamicNames
7402
+ dynamicNames,
7403
+ loaders
6576
7404
  };
6577
7405
  }
6578
7406
  function namedChildren(node) {
@@ -6640,6 +7468,7 @@ function factsForFile(options) {
6640
7468
  const db = new import_datalog3.Database();
6641
7469
  const bodyBlocks = bodyBlocksIn(options.packs);
6642
7470
  emitValueFacts(db, options.file, options.root, bodyBlocks);
7471
+ emitEnvFacts(db, options.file, options.root);
6643
7472
  emitConstantBindings(db, [
6644
7473
  collectFileConstants(
6645
7474
  options.file,
@@ -6653,9 +7482,22 @@ function factsForFile(options) {
6653
7482
  files: [{ file: options.file, root: options.root }],
6654
7483
  definitions: new Map(methodDefinitionsIn(options.file, options.root))
6655
7484
  });
6656
- (0, import_resolution8.addPackWords)(db, packWordsOf(options.packs));
7485
+ (0, import_resolution9.addPackWords)(db, packWordsOf(options.packs));
6657
7486
  return db;
6658
7487
  }
7488
+ function moduleInitUnit(options) {
7489
+ const raw = (0, import_extractor10.moduleInitStructure)({
7490
+ name: options.name,
7491
+ file: options.file,
7492
+ range: options.range,
7493
+ effects: options.effects
7494
+ });
7495
+ const branch = raw.branches[0];
7496
+ if (branch !== void 0) {
7497
+ branch.effects = options.calls;
7498
+ }
7499
+ return raw;
7500
+ }
6659
7501
  function alreadyDiscovered(seen, raw) {
6660
7502
  const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
6661
7503
  const boundary = raw.boundaryBinding === null ? "" : (0, import_behavioral_ir5.boundaryKey)(raw.boundaryBinding) ?? "";
@@ -6711,6 +7553,7 @@ async function extractRubyProject(options) {
6711
7553
  }
6712
7554
  parsed.push({ file, root });
6713
7555
  emitValueFacts(db, file, root, bodyBlocks);
7556
+ emitEnvFacts(db, file, root);
6714
7557
  for (const [key, method] of methodDefinitionsIn(file, root)) {
6715
7558
  definitions.set(key, method);
6716
7559
  }
@@ -6726,7 +7569,16 @@ async function extractRubyProject(options) {
6726
7569
  emitRequireFacts(db, file, root, known);
6727
7570
  }
6728
7571
  bindEvaluator(db, { files: parsed, definitions });
6729
- (0, import_resolution8.addPackWords)(db, packWordsOf(options.packs));
7572
+ (0, import_resolution9.addPackWords)(db, packWordsOf(options.packs));
7573
+ const callbacks = callbacksIn(
7574
+ options.packs.flatMap((pack) => pack.storage ?? [])
7575
+ );
7576
+ for (const { file, root } of parsed) {
7577
+ walkDefinitions(
7578
+ root,
7579
+ (info) => emitClassCallbacks(db, file, info.node, callbacks)
7580
+ );
7581
+ }
6730
7582
  return readDynamicNames(
6731
7583
  db,
6732
7584
  new Map(parsed.map(({ file, root }) => [file, root]))
@@ -6734,11 +7586,17 @@ async function extractRubyProject(options) {
6734
7586
  });
6735
7587
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
6736
7588
  const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
6737
- const storage = storagePatterns.length > 0 ? { facts: db, patterns: storagePatterns, loaders: loaderPatterns } : void 0;
7589
+ const rawSqlPatterns = options.packs.flatMap((pack) => pack.rawSql ?? []);
7590
+ const storage = storagePatterns.length > 0 || rawSqlPatterns.length > 0 ? {
7591
+ facts: db,
7592
+ patterns: storagePatterns,
7593
+ loaders: loaderPatterns,
7594
+ rawSql: rawSqlPatterns
7595
+ } : void 0;
6738
7596
  const inheritedMethods = inheritedMethodsIn(options.packs);
6739
7597
  const reachContext = await timer.timeAsync(
6740
7598
  "discover",
6741
- () => buildReachContext(parsed, db, bodyBlocks, dynamicNames)
7599
+ () => buildReachContext(parsed, db, bodyBlocks, dynamicNames, loaderPatterns)
6742
7600
  );
6743
7601
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
6744
7602
  const seeds = [];
@@ -6788,23 +7646,34 @@ async function extractRubyProject(options) {
6788
7646
  });
6789
7647
  }
6790
7648
  }
6791
- const loadTimeReads = timer.time("discover", () => envReadEffects(root));
6792
- if (loadTimeReads.length > 0) {
6793
- const summary = timer.time(
6794
- "summarize",
6795
- () => (0, import_extractor10.assembleSummary)(
6796
- (0, import_extractor10.moduleInitStructure)({
6797
- name: import_node_path4.default.basename(displayPath),
6798
- file: displayPath,
6799
- range: rangeOf(root),
6800
- effects: loadTimeReads
6801
- }),
6802
- { gapHandling: "permissive" }
6803
- )
6804
- );
6805
- summary.confidence = { source: "inferred_static", level: "low" };
6806
- found.push({ summary });
6807
- }
7649
+ const moduleKey = nodeId(file, root);
7650
+ seedKeys.add(moduleKey);
7651
+ seeds.push({
7652
+ key: moduleKey,
7653
+ file,
7654
+ node: root,
7655
+ enclosingQualifiedName: null
7656
+ });
7657
+ const loadTime = timer.time("discover", () => ({
7658
+ reads: envReadEffects(root, { db, file }),
7659
+ calls: moduleScopeInvocationEffects(
7660
+ root,
7661
+ inheritedMethods,
7662
+ EVERY_ARGLESS_CALL,
7663
+ db
7664
+ )
7665
+ }));
7666
+ found.push({
7667
+ raw: moduleInitUnit({
7668
+ name: import_node_path4.default.basename(displayPath),
7669
+ file: displayPath,
7670
+ range: rangeOf(root),
7671
+ effects: loadTime.reads,
7672
+ calls: loadTime.calls
7673
+ }),
7674
+ seedKey: moduleKey,
7675
+ onlyIfItReaches: loadTime.reads.length === 0
7676
+ });
6808
7677
  }
6809
7678
  for (const pack of options.packs) {
6810
7679
  for (const pattern of pack.discovery) {
@@ -6842,6 +7711,9 @@ async function extractRubyProject(options) {
6842
7711
  continue;
6843
7712
  }
6844
7713
  const { raw, seedKey } = entry;
7714
+ if (entry.onlyIfItReaches === true && (seedKey === null || !reached.followedKeys.has(seedKey))) {
7715
+ continue;
7716
+ }
6845
7717
  if (seedKey !== null) {
6846
7718
  dropPropertyReads(raw, reached.propertyReadsByKey.get(seedKey));
6847
7719
  }
@@ -6950,21 +7822,10 @@ function findRubyFiles(root) {
6950
7822
  }
6951
7823
 
6952
7824
  // src/facts/why.ts
6953
- var WITNESS_RULES = [...import_resolution9.RESOLUTION_RULES, ...RUBY_RULES];
6954
- var METHOD_TYPES5 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
7825
+ var WITNESS_RULES = [...import_resolution10.RESOLUTION_RULES, ...RUBY_RULES];
6955
7826
  function namedChildrenOf(node) {
6956
7827
  return node.namedChildren.filter((child) => child !== null);
6957
7828
  }
6958
- function enclosingMethod2(node) {
6959
- let current = node.parent;
6960
- while (current !== null) {
6961
- if (METHOD_TYPES5.has(current.type)) {
6962
- return current;
6963
- }
6964
- current = current.parent;
6965
- }
6966
- return null;
6967
- }
6968
7829
  function indexFile(file, root, locations) {
6969
7830
  const walk = (node) => {
6970
7831
  locations.set(nodeId(file, node), { file, node });
@@ -7000,7 +7861,7 @@ function width(node) {
7000
7861
  return node.endIndex - node.startIndex;
7001
7862
  }
7002
7863
  function askedKey(value) {
7003
- const enclosing = enclosingMethod2(value.node);
7864
+ const enclosing = enclosingDefinition(value.node);
7004
7865
  return calleeKeyOf(value.file, value.node, enclosing) ?? readKey(value.file, value.node, enclosing);
7005
7866
  }
7006
7867
  var RubyWhySession = class {
@@ -7075,7 +7936,7 @@ var RubyWhySession = class {
7075
7936
  * which the caller says in its own words.
7076
7937
  */
7077
7938
  explain(value, options = {}) {
7078
- return (0, import_resolution9.explainResolvedKey)({
7939
+ return (0, import_resolution10.explainResolvedKey)({
7079
7940
  db: this.db,
7080
7941
  rules: WITNESS_RULES,
7081
7942
  key: askedKey(value),
@@ -7216,6 +8077,8 @@ async function rubyStubEvidence(options) {
7216
8077
  resolveConstantFile,
7217
8078
  rubyStubEvidence,
7218
8079
  runStatements,
8080
+ storageClaims,
8081
+ storageEffects,
7219
8082
  stringLiteralValue,
7220
8083
  stringValueOf,
7221
8084
  symbolValue,