@suss/adapter-ruby 0.32.0 → 0.33.1

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,12 +145,19 @@ 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"]);
147
155
  var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
148
- function enclosingMethod(node) {
156
+ var LAMBDA_TYPE = "lambda";
157
+ function enclosingDefinition(node) {
149
158
  let current = node.parent;
150
159
  while (current !== null) {
151
- if (METHOD_TYPES.has(current.type)) {
160
+ if (METHOD_TYPES.has(current.type) || current.type === LAMBDA_TYPE) {
152
161
  return current;
153
162
  }
154
163
  current = current.parent;
@@ -471,11 +480,14 @@ var RUBY_PROGRAM = (0, import_resolution.resolutionProgram)(RUBY_RULES);
471
480
  function resolveValues(db, keys) {
472
481
  (0, import_resolution.askResolution)(db, keys, "wanted", RUBY_PROGRAM);
473
482
  }
474
- function resolveEnvSites(db, sites) {
475
- (0, import_resolution.askResolution)(db, sites, "wantedEnvSite", RUBY_PROGRAM);
483
+ function resolveEnvObjects(db, objects) {
484
+ (0, import_resolution.askResolution)(db, objects, "wantedEnvObject", RUBY_PROGRAM);
476
485
  }
477
486
  function resolvedFunctions(db, key) {
478
- return db.lookup("wantedResolves", 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]));
479
491
  }
480
492
  function writtenValueOf(db, key) {
481
493
  resolveValues(db, [key]);
@@ -575,12 +587,15 @@ var BINDS_A_NAME = {
575
587
  PARAMETER_LIST_TYPES.map((type) => [type, parameterNames])
576
588
  )
577
589
  };
578
- function localNamesIn(definitionNode) {
590
+ var DESCENDS_EVERYWHERE = /* @__PURE__ */ new Set();
591
+ function localNamesIn(definitionNode, stops = DESCENDS_EVERYWHERE) {
579
592
  const names = /* @__PURE__ */ new Set();
580
593
  const visit = (node) => {
581
594
  BINDS_A_NAME[node.type]?.(node, names);
582
595
  for (const child of bodyStatements(node)) {
583
- visit(child);
596
+ if (!stops.has(child.type)) {
597
+ visit(child);
598
+ }
584
599
  }
585
600
  };
586
601
  visit(definitionNode);
@@ -593,11 +608,12 @@ function spellsAName(node) {
593
608
  }
594
609
  return SPELLS_A_NAME[parent.type]?.(parent, node) === true;
595
610
  }
611
+ var KEYWORD_LITERALS = /* @__PURE__ */ new Set(["__FILE__", "__LINE__", "__ENCODING__"]);
596
612
  function isBareMethodCall(node, locals) {
597
613
  if (node.type !== "identifier" || locals.has(node.text)) {
598
614
  return false;
599
615
  }
600
- return !spellsAName(node);
616
+ return !KEYWORD_LITERALS.has(node.text) && !spellsAName(node);
601
617
  }
602
618
 
603
619
  // src/facts/locals.ts
@@ -748,25 +764,46 @@ function namesOfFile(tree) {
748
764
  cache.file ??= scopeNames(/* @__PURE__ */ new Set(), tree.rootNode);
749
765
  return cache.file;
750
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);
751
778
  function ownerOfName(node, name, enclosing) {
752
779
  if (node.type === "constant") {
753
780
  return null;
754
781
  }
755
- const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
756
- if (names.blockParams.has(name)) {
757
- const block = blockDeclaring(node, name);
758
- if (block !== null) {
759
- 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;
760
792
  }
761
793
  }
762
- return enclosing !== null && names.locals.has(name) ? enclosing : null;
794
+ return null;
763
795
  }
764
796
  function isLocalName(node, name, enclosing) {
765
- const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
766
- if (names.blockParams.has(name) && blockDeclaring(node, name) !== null) {
767
- 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
+ }
768
805
  }
769
- return names.locals.has(name);
806
+ return false;
770
807
  }
771
808
  function blockDeclaring(node, name) {
772
809
  let current = node.parent;
@@ -957,19 +994,33 @@ function calleeKeyOf(filePath, node, enclosing) {
957
994
  }
958
995
  return field(node, "receiver") === null ? nameKey(filePath, method, enclosing) : nodeId(filePath, method);
959
996
  }
997
+ function invokedKeyOf(filePath, node, enclosing) {
998
+ const invoked = invokedValueOf(node);
999
+ return invoked === null ? null : readKey(filePath, invoked, enclosing);
1000
+ }
960
1001
  function emitCall(emitter, call) {
961
- 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) {
962
1014
  const method = call.type === "identifier" ? call : field(call, "method");
963
1015
  const calleeKey = calleeKeyOf(emitter.filePath, call, emitter.enclosing);
964
1016
  if (method === null || calleeKey === null) {
965
- return;
1017
+ return false;
966
1018
  }
967
- const receiver = field(call, "receiver");
968
- const callKey = nodeId(emitter.filePath, call);
969
1019
  add(emitter, "call", callKey, calleeKey);
970
1020
  if (!emitter.insideMethod) {
971
1021
  add(emitter, "callOutsideMethod", callKey);
972
1022
  }
1023
+ const receiver = field(call, "receiver");
973
1024
  if (receiver !== null) {
974
1025
  add(
975
1026
  emitter,
@@ -981,6 +1032,21 @@ function emitCall(emitter, call) {
981
1032
  } else if (emitter.selfKey !== null) {
982
1033
  add(emitter, "readsProperty", calleeKey, emitter.selfKey, method.text);
983
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) {
984
1050
  const args = field(call, "arguments");
985
1051
  let position = 0;
986
1052
  for (const argument of args === null ? [] : children(args)) {
@@ -1100,12 +1166,58 @@ function walkExpressions(node, emitter, visit, turns = []) {
1100
1166
  );
1101
1167
  }
1102
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
+ }
1103
1211
  function emitExpressionFacts(emitter, node) {
1104
1212
  emitDynamicDefinition(emitter, node, []);
1105
1213
  walkExpressions(node, emitter, (child, turns) => {
1106
1214
  emitDynamicDefinition(emitter, child, turns);
1107
1215
  if (child.type === "call" || isBareCall(child, emitter.enclosing)) {
1108
1216
  emitCall(emitter, child);
1217
+ emitKeyedFetch(emitter, child);
1218
+ }
1219
+ if (child.type === "element_reference") {
1220
+ emitKeyedElement(emitter, child);
1109
1221
  }
1110
1222
  if (ARRAY_TYPES.has(child.type)) {
1111
1223
  emitArray(emitter, child);
@@ -1229,6 +1341,25 @@ function assignedValueOf(node) {
1229
1341
  }
1230
1342
  return field(node, "right");
1231
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
+ }
1232
1363
  function emitMethodFacts(emitter, method) {
1233
1364
  const funcKey = nodeId(emitter.filePath, method);
1234
1365
  add(emitter, "func", funcKey);
@@ -1243,7 +1374,7 @@ function emitMethodFacts(emitter, method) {
1243
1374
  position += 1;
1244
1375
  }
1245
1376
  const inside = { ...emitter, enclosing: method };
1246
- const body = field(method, "body");
1377
+ const body = definitionBody(method);
1247
1378
  if (body === null) {
1248
1379
  return funcKey;
1249
1380
  }
@@ -1275,6 +1406,7 @@ function emitMethodFacts(emitter, method) {
1275
1406
  }
1276
1407
  emitExpressionFacts(inside, body);
1277
1408
  emitScopeWrites(inside, method, body);
1409
+ emitLambdasIn(inside, body);
1278
1410
  return funcKey;
1279
1411
  }
1280
1412
  function isConstruction(value) {
@@ -1434,12 +1566,15 @@ function emitClassFacts(emitter, cls) {
1434
1566
  add(emitter, "initializes", classKey, funcKey);
1435
1567
  }
1436
1568
  }
1437
- const methodBody = field(statement, "body");
1438
- if (methodBody !== null) {
1439
- emitInstanceStores(within, funcKey, methodBody, stored);
1569
+ const methodBody2 = field(statement, "body");
1570
+ if (methodBody2 !== null) {
1571
+ emitInstanceStores(within, funcKey, methodBody2, stored);
1440
1572
  }
1441
1573
  }
1442
1574
  emitInstanceWrites(within, classKey, collected);
1575
+ if (body !== null) {
1576
+ emitLambdasIn(within, body);
1577
+ }
1443
1578
  return classKey;
1444
1579
  }
1445
1580
  function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
@@ -1475,6 +1610,7 @@ function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
1475
1610
  walk(root);
1476
1611
  emitExpressionFacts(emitter, root);
1477
1612
  emitScopeWrites(emitter, null, root);
1613
+ emitLambdasIn(emitter, root);
1478
1614
  }
1479
1615
 
1480
1616
  // src/values/lowering.ts
@@ -2476,6 +2612,24 @@ function writtenNodeOf(node, db, site) {
2476
2612
  const context = db === void 0 ? void 0 : contexts.get(db);
2477
2613
  return context === void 0 ? null : context.writtenTo(node, site);
2478
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
+ }
2479
2633
  function evaluatedValue(node, db, bindings, site) {
2480
2634
  const evaluator = evaluatorFor(node, db);
2481
2635
  if (bindings === void 0) {
@@ -2556,7 +2710,7 @@ function contextOver(db, nodes) {
2556
2710
  if (file === null) {
2557
2711
  return null;
2558
2712
  }
2559
- const key = readKey(file, node, enclosingMethod(node));
2713
+ const key = readKey(file, node, enclosingDefinition(node));
2560
2714
  const answer = site === void 0 ? writtenValueOf(db, key) : writtenValueUnder(db, key, site);
2561
2715
  return answer === null ? null : (0, import_resolution4.nodeOfKey)(rootsByFile, answer);
2562
2716
  },
@@ -2566,7 +2720,7 @@ function contextOver(db, nodes) {
2566
2720
  if (file === null || method === null) {
2567
2721
  return null;
2568
2722
  }
2569
- 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);
2570
2724
  resolveValues(db, [key]);
2571
2725
  const resolved = resolvedFunctions(db, key);
2572
2726
  const settled = resolved.length === 1 ? resolved[0] : void 0;
@@ -2718,8 +2872,8 @@ function qualifyConstantRef(node, nesting) {
2718
2872
  return compoundName(node);
2719
2873
  }
2720
2874
  if (node.type === "constant") {
2721
- const innermost = nesting[0] ?? null;
2722
- return innermost === null ? node.text : `${innermost}::${node.text}`;
2875
+ const innermost2 = nesting[0] ?? null;
2876
+ return innermost2 === null ? node.text : `${innermost2}::${node.text}`;
2723
2877
  }
2724
2878
  return null;
2725
2879
  }
@@ -2776,9 +2930,9 @@ function ownIdentity(nameNode, outerNesting) {
2776
2930
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
2777
2931
  }
2778
2932
  function definedName(nameNode, outerNesting) {
2779
- const innermost = outerNesting[0] ?? null;
2780
- if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
2781
- 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}`;
2782
2936
  }
2783
2937
  return qualifyConstantRef(nameNode, outerNesting);
2784
2938
  }
@@ -3402,24 +3556,38 @@ function isCall(node, locals, inherited) {
3402
3556
  }
3403
3557
  return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
3404
3558
  }
3405
- function collectCalls(node, locals, inherited, found) {
3559
+ function collectCalls(node, read, inherited, found) {
3406
3560
  for (const child of children2(node)) {
3407
- if (OWN_BODY_TYPES.has(child.type)) {
3561
+ if (read.stops.has(child.type)) {
3408
3562
  continue;
3409
3563
  }
3410
- if (isCall(child, locals, inherited)) {
3564
+ if (isCall(child, read.locals, inherited)) {
3411
3565
  found.push(child);
3412
3566
  }
3413
- collectCalls(child, locals, inherited, found);
3567
+ collectCalls(child, read, inherited, found);
3414
3568
  }
3415
3569
  return found;
3416
3570
  }
3417
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
3571
+ function methodBody(definitionNode) {
3418
3572
  const body = field(definitionNode, "body");
3419
3573
  if (body === null) {
3420
- return [];
3574
+ return null;
3421
3575
  }
3422
- 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, []);
3423
3591
  }
3424
3592
  function calleeText(call) {
3425
3593
  const receiver = field(call, "receiver");
@@ -3463,14 +3631,23 @@ function enclosingStatement(call, body) {
3463
3631
  return current;
3464
3632
  }
3465
3633
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
3466
- const body = field(definitionNode, "body");
3467
- if (body === null) {
3634
+ const read = methodBody(definitionNode);
3635
+ if (read === null) {
3468
3636
  return [];
3469
3637
  }
3470
- const calls = callsReported(
3471
- bodyCalls(definitionNode, inherited),
3472
- 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
3473
3646
  );
3647
+ }
3648
+ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
3649
+ const body = read.body;
3650
+ const calls = callsReported(bodyCalls(read, inherited), keepsArglessCall);
3474
3651
  if (calls.length === 0) {
3475
3652
  return [];
3476
3653
  }
@@ -3876,6 +4053,57 @@ function clientCallUnits(root, pack, pattern, options) {
3876
4053
  }
3877
4054
  return units;
3878
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
+ }
3879
4107
  function methodDefinitions(node, found = []) {
3880
4108
  for (const child of node.namedChildren) {
3881
4109
  if (child === null) {
@@ -4000,9 +4228,11 @@ function builderCallBehind(receiver, pattern, facts) {
4000
4228
  return constant2 !== null && namesConstant(constant2, pattern.constantName) ? written : null;
4001
4229
  }
4002
4230
  function urlIn(args, pattern, options, site) {
4231
+ return pathAt(urlNodeIn(args, pattern), options, site);
4232
+ }
4233
+ function urlNodeIn(args, pattern) {
4003
4234
  const keyword = pattern.url.keyword;
4004
- const written = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
4005
- return pathAt(written, options, site);
4235
+ return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
4006
4236
  }
4007
4237
  function pathAt(written, options, site) {
4008
4238
  if (written === void 0) {
@@ -4124,11 +4354,11 @@ function namedParameters(db) {
4124
4354
  }
4125
4355
  const named2 = /* @__PURE__ */ new Map();
4126
4356
  namedByDb.set(db, named2);
4127
- const sites = db.facts("readsEnvNamed").map((row) => String(row[0]));
4128
- if (sites.length === 0) {
4357
+ const objects = db.facts("environmentObject").map((row) => String(row[0]));
4358
+ if (objects.length === 0) {
4129
4359
  return named2;
4130
4360
  }
4131
- resolveEnvSites(db, sites);
4361
+ resolveEnvObjects(db, objects);
4132
4362
  for (const row of db.facts("wantedParamNamesEnv")) {
4133
4363
  const found = named2.get(String(row[0]));
4134
4364
  if (found === void 0) {
@@ -4139,38 +4369,44 @@ function namedParameters(db) {
4139
4369
  }
4140
4370
  return named2;
4141
4371
  }
4142
- function emitEnvNameFacts(db, file, root) {
4143
- for (const site of envNameSites(root)) {
4144
- const key = nodeId(file, site.read);
4145
- db.add("readsEnvNamed", [
4146
- key,
4147
- readKey(file, site.name, enclosingMethod(site.name))
4148
- ]);
4149
- if (site.defaulted) {
4150
- db.add(ENV_DEFAULTED, [key]);
4151
- }
4152
- }
4153
- }
4154
- function envNameSites(root) {
4155
- const sites = [];
4372
+ function emitEnvFacts(db, file, root) {
4156
4373
  (0, import_extractor4.walkDescendants)(root, null, {
4157
4374
  at: (node) => {
4375
+ if (isEnv(node) && handsOnward(node)) {
4376
+ db.add("environmentObject", [
4377
+ readKey(file, node, enclosingDefinition(node))
4378
+ ]);
4379
+ }
4158
4380
  const site = envSiteAt(node);
4159
- if (site !== null && stringLiteralValue(site.name) === null) {
4160
- sites.push(site);
4381
+ if (site !== null && site.defaulted && stringLiteralValue(site.name) === null) {
4382
+ db.add(ENV_DEFAULTED, [nodeId(file, site.read)]);
4161
4383
  }
4162
4384
  },
4163
4385
  into: () => null
4164
4386
  });
4165
- return sites;
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;
4166
4398
  }
4167
4399
  function helperReads(calls, facts) {
4168
4400
  const { db, file, named: named2 } = facts;
4169
4401
  const callees = new NodeMap();
4170
4402
  for (const call of calls) {
4171
- const key = calleeKeyOf(file, call, enclosingMethod(call));
4172
- if (key !== null) {
4173
- callees.set(call, key);
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);
4174
4410
  }
4175
4411
  }
4176
4412
  const reads = new NodeMap();
@@ -4179,7 +4415,7 @@ function helperReads(calls, facts) {
4179
4415
  }
4180
4416
  resolveValues(
4181
4417
  db,
4182
- [...callees].map(([, callee]) => callee)
4418
+ [...callees].flatMap(([, keys]) => keys)
4183
4419
  );
4184
4420
  for (const [call, callee] of callees) {
4185
4421
  const args = namingArguments(db, named2, call, callee);
@@ -4193,10 +4429,10 @@ function helperReads(calls, facts) {
4193
4429
  }
4194
4430
  return reads;
4195
4431
  }
4196
- function namingArguments(db, named2, call, callee) {
4432
+ function namingArguments(db, named2, call, callees) {
4197
4433
  const { positional, keyword } = readCallArgs(field(call, "arguments"));
4198
4434
  const found = [];
4199
- for (const func of resolvedFunctions(db, callee)) {
4435
+ for (const func of callees.flatMap((key) => resolvedFunctions(db, key))) {
4200
4436
  for (const row of db.lookup("paramOf", 0, func)) {
4201
4437
  const sites = named2.get(String(row[2]));
4202
4438
  const argument = positional[Number(row[1])];
@@ -4460,6 +4696,9 @@ function methodNamesOf(args, filter, facts) {
4460
4696
  const value = named2 === void 0 ? null : stringValueOf(named2, facts);
4461
4697
  return value === null ? [] : [value];
4462
4698
  }
4699
+ return symbolArgumentNames(args, facts);
4700
+ }
4701
+ function symbolArgumentNames(args, facts) {
4463
4702
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
4464
4703
  }
4465
4704
  function skipsOf(statement, filterName, facts) {
@@ -4498,19 +4737,315 @@ function actionsUnder(node, facts) {
4498
4737
  }
4499
4738
 
4500
4739
  // src/storage.ts
4501
- 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
4502
4744
  var import_resolution6 = require("@suss/resolution");
4503
- function children3(node) {
4504
- 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])));
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;
4505
4765
  }
4766
+
4767
+ // src/loaders.ts
4506
4768
  function receiverOf(node) {
4507
4769
  return node.type === "call" ? field(node, "receiver") : null;
4508
4770
  }
4509
4771
  function methodOf(node) {
4510
4772
  return field(node, "method")?.text ?? "";
4511
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
+ }
4512
5047
  function rootConstant(node) {
4513
- const receiver = receiverOf(node);
5048
+ const receiver = receiverOf2(node);
4514
5049
  if (receiver === null) {
4515
5050
  return null;
4516
5051
  }
@@ -4525,17 +5060,6 @@ function isConstant(node) {
4525
5060
  function constantName(constant2) {
4526
5061
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
4527
5062
  }
4528
- function reachesBase(facts, classKey, bases) {
4529
- (0, import_resolution6.askResolution)(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4530
- return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4531
- }
4532
- function classBehind(facts, file, constant2) {
4533
- const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4534
- const bound = new Set(
4535
- facts.lookup("binds", 0, key).map((row) => String(row[1]))
4536
- );
4537
- return bound.size === 1 ? [...bound][0] : void 0;
4538
- }
4539
5063
  function modelPattern(constant2, file, options) {
4540
5064
  const classKey = classBehind(options.facts, file, constant2);
4541
5065
  if (classKey === void 0) {
@@ -4550,7 +5074,7 @@ function chainLinks(call) {
4550
5074
  let current = call;
4551
5075
  while (current !== null && current.type === "call") {
4552
5076
  links.push(current);
4553
- current = receiverOf(current);
5077
+ current = receiverOf2(current);
4554
5078
  }
4555
5079
  return links;
4556
5080
  }
@@ -4563,9 +5087,17 @@ function kindOfCall(pattern, method) {
4563
5087
  }
4564
5088
  return void 0;
4565
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
+ }
4566
5098
  function someLibraryDefines(options, method) {
4567
5099
  return options.patterns.some(
4568
- (pattern) => kindOfCall(pattern, method) !== void 0
5100
+ (pattern) => libraryCallOf(pattern, method) !== void 0
4569
5101
  );
4570
5102
  }
4571
5103
  function argumentsOf2(call) {
@@ -4600,7 +5132,7 @@ function hasPositionalArgument(call, facts) {
4600
5132
  );
4601
5133
  }
4602
5134
  function columnsAskedFor(call, pattern, facts) {
4603
- if (!(pattern.columnArguments ?? []).includes(methodOf(call))) {
5135
+ if (!(pattern.columnArguments ?? []).includes(methodOf2(call))) {
4604
5136
  return [];
4605
5137
  }
4606
5138
  return argumentsOf2(call).map((argument) => stringValueOf(argument, facts)).filter((column) => column !== null);
@@ -4610,14 +5142,14 @@ function primaryKeySelector(call, pattern, facts) {
4610
5142
  if (byPrimaryKey === void 0) {
4611
5143
  return [];
4612
5144
  }
4613
- const picks = byPrimaryKey.methods.includes(methodOf(call)) && hasPositionalArgument(call, facts);
5145
+ const picks = byPrimaryKey.methods.includes(methodOf2(call)) && hasPositionalArgument(call, facts);
4614
5146
  return picks ? [byPrimaryKey.column] : [];
4615
5147
  }
4616
5148
  function selectorOf(call, pattern, facts) {
4617
5149
  const picked = [];
4618
5150
  for (const link of chainLinks(call)) {
4619
5151
  picked.push(...primaryKeySelector(link, pattern, facts));
4620
- if (pattern.reads.includes(methodOf(link))) {
5152
+ if (pattern.reads.includes(methodOf2(link))) {
4621
5153
  picked.push(...keywordKeys(link, facts));
4622
5154
  }
4623
5155
  }
@@ -4634,10 +5166,10 @@ function fieldsOf(call, pattern, kind, facts) {
4634
5166
  ];
4635
5167
  }
4636
5168
  function storageEffect(call, container, pattern, kind, selector, fields) {
4637
- const operation = methodOf(call);
5169
+ const operation = methodOf2(call);
4638
5170
  return {
4639
5171
  type: "interaction",
4640
- binding: (0, import_ir_core.storageBinding)({
5172
+ binding: (0, import_ir_core2.storageBinding)({
4641
5173
  recognition: "ruby-storage",
4642
5174
  storageSystem: pattern.storageSystem,
4643
5175
  scope: "default",
@@ -4654,7 +5186,7 @@ function storageEffect(call, container, pattern, kind, selector, fields) {
4654
5186
  };
4655
5187
  }
4656
5188
  function classSettledOn(facts, key) {
4657
- (0, import_resolution6.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
5189
+ (0, import_resolution7.askResolution)(facts, [key], "wanted", RUBY_PROGRAM);
4658
5190
  const objects = new Set(
4659
5191
  facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
4660
5192
  );
@@ -4686,7 +5218,7 @@ function receiverClass(call, file, options, enclosing) {
4686
5218
  if (constant2 !== null) {
4687
5219
  return constantReceiver(constant2, file, options.facts);
4688
5220
  }
4689
- const receiver = receiverOf(call);
5221
+ const receiver = receiverOf2(call);
4690
5222
  if (receiver === null) {
4691
5223
  return void 0;
4692
5224
  }
@@ -4694,60 +5226,91 @@ function receiverClass(call, file, options, enclosing) {
4694
5226
  }
4695
5227
  function libraryCallIn(call, options) {
4696
5228
  return chainLinks(call).find(
4697
- (link) => someLibraryDefines(options, methodOf(link))
5229
+ (link) => someLibraryDefines(options, methodOf2(link))
4698
5230
  ) ?? null;
4699
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
+ }
4700
5264
  function modelCallEffects(call, file, options, enclosing) {
4701
5265
  const worked = libraryCallIn(call, options);
4702
5266
  if (worked === null) {
4703
5267
  return [];
4704
5268
  }
4705
- const method = methodOf(worked);
5269
+ const method = methodOf2(worked);
4706
5270
  const target = receiverClass(worked, file, options, enclosing);
4707
5271
  if (target === void 0) {
4708
5272
  return [];
4709
5273
  }
4710
5274
  for (const pattern of options.patterns) {
4711
- const kind = kindOfCall(pattern, method);
4712
- 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)) {
4713
5277
  continue;
4714
5278
  }
4715
5279
  if (projectDeclares(options.facts, target.classKey, method)) {
4716
5280
  return [];
4717
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) : [];
4718
5286
  return [
4719
5287
  storageEffect(
4720
5288
  worked,
4721
5289
  target.container,
4722
5290
  pattern,
4723
- kind,
5291
+ library.kind,
4724
5292
  selectorOf(worked, pattern, options.facts),
4725
- fieldsOf(worked, pattern, kind, options.facts)
4726
- )
5293
+ fieldsOf(worked, pattern, library.kind, options.facts)
5294
+ ),
5295
+ ...ran.map(callbackEffect)
4727
5296
  ];
4728
5297
  }
4729
5298
  return [];
4730
5299
  }
4731
- function isLoader(node, loader) {
4732
- if (node === null) {
4733
- return false;
4734
- }
4735
- if (node.type === "identifier") {
4736
- return node.text === loader.loader;
4737
- }
4738
- return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
4739
- }
4740
- function loaderPick(call, loader) {
4741
- const method = methodOf(call);
4742
- const receiver = receiverOf(call);
4743
- if (receiver === null) {
4744
- 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 [];
4745
5305
  }
4746
- if (!loader.reads.includes(method) || receiver.type !== "call") {
4747
- 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);
4748
5312
  }
4749
- const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
4750
- return picks ? receiver : null;
5313
+ return [];
4751
5314
  }
4752
5315
  function loaderCallEffects(call, file, options) {
4753
5316
  const effects = [];
@@ -4773,10 +5336,24 @@ function loaderCallEffects(call, file, options) {
4773
5336
  }
4774
5337
  function effectsOfCall(call, file, options, enclosing) {
4775
5338
  const onModel = modelCallEffects(call, file, options, enclosing);
4776
- 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
+ });
4777
5351
  }
4778
5352
  function storageClaims(call, file, options, enclosing = null) {
4779
- 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;
4780
5357
  }
4781
5358
  function receiverKeysToAsk(chains, file, options, enclosing) {
4782
5359
  const keys = [];
@@ -4785,7 +5362,7 @@ function receiverKeysToAsk(chains, file, options, enclosing) {
4785
5362
  if (worked === null || rootConstant(worked) !== null) {
4786
5363
  continue;
4787
5364
  }
4788
- const receiver = receiverOf(worked);
5365
+ const receiver = receiverOf2(worked);
4789
5366
  if (receiver !== null) {
4790
5367
  keys.push(readKey(file, receiver, enclosing));
4791
5368
  }
@@ -4793,18 +5370,18 @@ function receiverKeysToAsk(chains, file, options, enclosing) {
4793
5370
  return keys;
4794
5371
  }
4795
5372
  function storageEffects(calls, file, options, enclosing = null) {
4796
- if (options.patterns.length === 0) {
5373
+ if (!saysAnything(options)) {
4797
5374
  return [];
4798
5375
  }
4799
5376
  const partOfOne = /* @__PURE__ */ new Set();
4800
5377
  for (const call of calls) {
4801
- const receiver = receiverOf(call);
5378
+ const receiver = receiverOf2(call);
4802
5379
  if (receiver !== null) {
4803
5380
  partOfOne.add(receiver.id);
4804
5381
  }
4805
5382
  }
4806
5383
  const chains = calls.filter((call) => !partOfOne.has(call.id));
4807
- (0, import_resolution6.askResolution)(
5384
+ (0, import_resolution7.askResolution)(
4808
5385
  options.facts,
4809
5386
  receiverKeysToAsk(chains, file, options, enclosing),
4810
5387
  "wanted",
@@ -4929,14 +5506,18 @@ async function discoverUnits(root, options) {
4929
5506
  file: options.absoluteFile ?? options.filePath
4930
5507
  }));
4931
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
+ );
4932
5518
  for (const pack of options.packs) {
4933
5519
  for (const pattern of pack.clients ?? []) {
4934
- units.push(
4935
- ...clientCallUnits(root, pack, pattern, {
4936
- filePath: options.filePath,
4937
- ...options.facts === void 0 ? {} : { facts: options.facts }
4938
- })
4939
- );
5520
+ units.push(...clientCallUnits(root, pack, pattern, clientOptions));
4940
5521
  }
4941
5522
  }
4942
5523
  for (const info of classes) {
@@ -5946,7 +6527,7 @@ function emitConstantBindings(db, perFile) {
5946
6527
  var import_node_fs4 = __toESM(require("fs"), 1);
5947
6528
  var import_node_path5 = __toESM(require("path"), 1);
5948
6529
  var import_datalog4 = require("@suss/datalog");
5949
- var import_resolution9 = require("@suss/resolution");
6530
+ var import_resolution10 = require("@suss/resolution");
5950
6531
 
5951
6532
  // src/parser.ts
5952
6533
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -6010,7 +6591,7 @@ var import_node_path4 = __toESM(require("path"), 1);
6010
6591
  var import_behavioral_ir5 = require("@suss/behavioral-ir");
6011
6592
  var import_datalog3 = require("@suss/datalog");
6012
6593
  var import_extractor10 = require("@suss/extractor");
6013
- var import_resolution8 = require("@suss/resolution");
6594
+ var import_resolution9 = require("@suss/resolution");
6014
6595
 
6015
6596
  // src/diagnostics.ts
6016
6597
  var import_extractor6 = require("@suss/extractor");
@@ -6027,6 +6608,47 @@ function buildRubyExtractionReport(args) {
6027
6608
  });
6028
6609
  }
6029
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
+
6030
6652
  // src/facts.ts
6031
6653
  var import_node_path3 = __toESM(require("path"), 1);
6032
6654
  var import_extractor7 = require("@suss/extractor");
@@ -6097,7 +6719,7 @@ var import_datalog2 = require("@suss/datalog");
6097
6719
  var import_extractor8 = require("@suss/extractor");
6098
6720
 
6099
6721
  // src/reach/resolveCallee.ts
6100
- var import_resolution7 = require("@suss/resolution");
6722
+ var import_resolution8 = require("@suss/resolution");
6101
6723
  var DYNAMIC_SEND_NAMES = /* @__PURE__ */ new Set(["send", "public_send", "__send__"]);
6102
6724
  var INVOKES_RECEIVER = "call";
6103
6725
  var STOP_FOR = {
@@ -6126,10 +6748,14 @@ function calleeSpellings(calls, ctx) {
6126
6748
  }
6127
6749
  return {
6128
6750
  spellingOf,
6129
- outcomes: (0, import_resolution7.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
6751
+ outcomes: (0, import_resolution8.calleeOutcomes)(ctx.facts, [...keys], RUBY_PROGRAM)
6130
6752
  };
6131
6753
  }
6132
6754
  function resolveCallee(call, site, ctx, read) {
6755
+ const throughLoader = loaderSourceCallee(call, site, ctx);
6756
+ if (throughLoader !== null) {
6757
+ return throughLoader;
6758
+ }
6133
6759
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
6134
6760
  if (spelling.kind === "stopped") {
6135
6761
  return spelling;
@@ -6139,12 +6765,27 @@ function resolveCallee(call, site, ctx, read) {
6139
6765
  }
6140
6766
  return asCallee(spelling, outcomeFor(spelling.key, ctx, read), site, ctx);
6141
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
+ }
6142
6783
  function outcomeFor(key, ctx, read) {
6143
- 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);
6144
6785
  }
6145
6786
  function mightReadAsACall(call, site, ctx) {
6146
6787
  const spelling = spellingFor(call, site);
6147
- 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);
6148
6789
  }
6149
6790
  function readsAsACall(call, site, ctx, read) {
6150
6791
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
@@ -6320,6 +6961,7 @@ async function reachedFunctions(seeds, options) {
6320
6961
  const parameterCallsByKey = /* @__PURE__ */ new Map();
6321
6962
  const propertyReadsByKey = /* @__PURE__ */ new Map();
6322
6963
  const passedPositions = /* @__PURE__ */ new Set();
6964
+ const followedKeys = /* @__PURE__ */ new Set();
6323
6965
  for (const seed of seeds) {
6324
6966
  seedKeys.add(seed.key);
6325
6967
  functionByKey.set(seed.key, {
@@ -6367,6 +7009,9 @@ async function reachedFunctions(seeds, options) {
6367
7009
  for (const position of scan.passedPositions) {
6368
7010
  passedPositions.add(position);
6369
7011
  }
7012
+ if (scan.followed.length > 0) {
7013
+ followedKeys.add(key);
7014
+ }
6370
7015
  for (const target of scan.followed) {
6371
7016
  const calleeKey = keyOf(target);
6372
7017
  if (!functionByKey.has(calleeKey)) {
@@ -6403,7 +7048,8 @@ async function reachedFunctions(seeds, options) {
6403
7048
  argTargetsByKey,
6404
7049
  parameterCallsByKey,
6405
7050
  propertyReadsByKey,
6406
- passedPositions
7051
+ passedPositions,
7052
+ followedKeys
6407
7053
  };
6408
7054
  }
6409
7055
  function keyOf(target) {
@@ -6442,13 +7088,9 @@ function methodReferenceSymbol(node) {
6442
7088
  return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
6443
7089
  }
6444
7090
  function bodyOf2(source, options) {
6445
- const site = {
6446
- file: source.file,
6447
- method: source.node,
6448
- owner: keyOf(source),
6449
- enclosingQualifiedName: source.enclosingQualifiedName
6450
- };
6451
- 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);
6452
7094
  const calls = callsReported(
6453
7095
  written,
6454
7096
  (call) => mightReadAsACall(call, site, options.context)
@@ -6460,6 +7102,17 @@ function bodyOf2(source, options) {
6460
7102
  written: calls.map((call) => ({ call, site }))
6461
7103
  };
6462
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
+ }
6463
7116
  function callsMade(asked, site, ctx, spellings) {
6464
7117
  return asked.filter(
6465
7118
  (call) => !isArglessReceiverCall(call) || readsAsACall(call, site, ctx, spellings)
@@ -6467,7 +7120,7 @@ function callsMade(asked, site, ctx, spellings) {
6467
7120
  }
6468
7121
  function scanBody(source, ctx, options, read) {
6469
7122
  const displayPathOf = options.displayPathOf;
6470
- if (field(source.node, "body") === null) {
7123
+ if (readableBodyOf(source.node) === null) {
6471
7124
  return EMPTY_SCAN;
6472
7125
  }
6473
7126
  const site = read.site;
@@ -6510,9 +7163,34 @@ function scanBody(source, ctx, options, read) {
6510
7163
  }
6511
7164
  }
6512
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
+ };
6513
7181
  for (const call of calls) {
6514
7182
  const callee = calleeText(call);
6515
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
+ }
6516
7194
  const placed = outcome.kind === "followed" ? {
6517
7195
  file: displayPathOf(outcome.target.file),
6518
7196
  span: spanOf(outcome.target.node)
@@ -6645,7 +7323,7 @@ function parameterName(param) {
6645
7323
 
6646
7324
  // src/reach/context.ts
6647
7325
  var METHOD_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
6648
- async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map()) {
7326
+ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map(), loaders = []) {
6649
7327
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
6650
7328
  const classes = [];
6651
7329
  const classNames = /* @__PURE__ */ new Map();
@@ -6721,7 +7399,8 @@ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dyna
6721
7399
  classNames,
6722
7400
  definitions,
6723
7401
  bodyBlocks,
6724
- dynamicNames
7402
+ dynamicNames,
7403
+ loaders
6725
7404
  };
6726
7405
  }
6727
7406
  function namedChildren(node) {
@@ -6789,7 +7468,7 @@ function factsForFile(options) {
6789
7468
  const db = new import_datalog3.Database();
6790
7469
  const bodyBlocks = bodyBlocksIn(options.packs);
6791
7470
  emitValueFacts(db, options.file, options.root, bodyBlocks);
6792
- emitEnvNameFacts(db, options.file, options.root);
7471
+ emitEnvFacts(db, options.file, options.root);
6793
7472
  emitConstantBindings(db, [
6794
7473
  collectFileConstants(
6795
7474
  options.file,
@@ -6803,9 +7482,22 @@ function factsForFile(options) {
6803
7482
  files: [{ file: options.file, root: options.root }],
6804
7483
  definitions: new Map(methodDefinitionsIn(options.file, options.root))
6805
7484
  });
6806
- (0, import_resolution8.addPackWords)(db, packWordsOf(options.packs));
7485
+ (0, import_resolution9.addPackWords)(db, packWordsOf(options.packs));
6807
7486
  return db;
6808
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
+ }
6809
7501
  function alreadyDiscovered(seen, raw) {
6810
7502
  const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
6811
7503
  const boundary = raw.boundaryBinding === null ? "" : (0, import_behavioral_ir5.boundaryKey)(raw.boundaryBinding) ?? "";
@@ -6861,7 +7553,7 @@ async function extractRubyProject(options) {
6861
7553
  }
6862
7554
  parsed.push({ file, root });
6863
7555
  emitValueFacts(db, file, root, bodyBlocks);
6864
- emitEnvNameFacts(db, file, root);
7556
+ emitEnvFacts(db, file, root);
6865
7557
  for (const [key, method] of methodDefinitionsIn(file, root)) {
6866
7558
  definitions.set(key, method);
6867
7559
  }
@@ -6877,7 +7569,16 @@ async function extractRubyProject(options) {
6877
7569
  emitRequireFacts(db, file, root, known);
6878
7570
  }
6879
7571
  bindEvaluator(db, { files: parsed, definitions });
6880
- (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
+ }
6881
7582
  return readDynamicNames(
6882
7583
  db,
6883
7584
  new Map(parsed.map(({ file, root }) => [file, root]))
@@ -6885,11 +7586,17 @@ async function extractRubyProject(options) {
6885
7586
  });
6886
7587
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
6887
7588
  const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
6888
- 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;
6889
7596
  const inheritedMethods = inheritedMethodsIn(options.packs);
6890
7597
  const reachContext = await timer.timeAsync(
6891
7598
  "discover",
6892
- () => buildReachContext(parsed, db, bodyBlocks, dynamicNames)
7599
+ () => buildReachContext(parsed, db, bodyBlocks, dynamicNames, loaderPatterns)
6893
7600
  );
6894
7601
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path4.default.relative(options.workspaceRoot, file) : file;
6895
7602
  const seeds = [];
@@ -6939,26 +7646,34 @@ async function extractRubyProject(options) {
6939
7646
  });
6940
7647
  }
6941
7648
  }
6942
- const loadTimeReads = timer.time(
6943
- "discover",
6944
- () => envReadEffects(root, { db, file })
6945
- );
6946
- if (loadTimeReads.length > 0) {
6947
- const summary = timer.time(
6948
- "summarize",
6949
- () => (0, import_extractor10.assembleSummary)(
6950
- (0, import_extractor10.moduleInitStructure)({
6951
- name: import_node_path4.default.basename(displayPath),
6952
- file: displayPath,
6953
- range: rangeOf(root),
6954
- effects: loadTimeReads
6955
- }),
6956
- { gapHandling: "permissive" }
6957
- )
6958
- );
6959
- summary.confidence = { source: "inferred_static", level: "low" };
6960
- found.push({ summary });
6961
- }
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
+ });
6962
7677
  }
6963
7678
  for (const pack of options.packs) {
6964
7679
  for (const pattern of pack.discovery) {
@@ -6996,6 +7711,9 @@ async function extractRubyProject(options) {
6996
7711
  continue;
6997
7712
  }
6998
7713
  const { raw, seedKey } = entry;
7714
+ if (entry.onlyIfItReaches === true && (seedKey === null || !reached.followedKeys.has(seedKey))) {
7715
+ continue;
7716
+ }
6999
7717
  if (seedKey !== null) {
7000
7718
  dropPropertyReads(raw, reached.propertyReadsByKey.get(seedKey));
7001
7719
  }
@@ -7104,7 +7822,7 @@ function findRubyFiles(root) {
7104
7822
  }
7105
7823
 
7106
7824
  // src/facts/why.ts
7107
- var WITNESS_RULES = [...import_resolution9.RESOLUTION_RULES, ...RUBY_RULES];
7825
+ var WITNESS_RULES = [...import_resolution10.RESOLUTION_RULES, ...RUBY_RULES];
7108
7826
  function namedChildrenOf(node) {
7109
7827
  return node.namedChildren.filter((child) => child !== null);
7110
7828
  }
@@ -7143,7 +7861,7 @@ function width(node) {
7143
7861
  return node.endIndex - node.startIndex;
7144
7862
  }
7145
7863
  function askedKey(value) {
7146
- const enclosing = enclosingMethod(value.node);
7864
+ const enclosing = enclosingDefinition(value.node);
7147
7865
  return calleeKeyOf(value.file, value.node, enclosing) ?? readKey(value.file, value.node, enclosing);
7148
7866
  }
7149
7867
  var RubyWhySession = class {
@@ -7218,7 +7936,7 @@ var RubyWhySession = class {
7218
7936
  * which the caller says in its own words.
7219
7937
  */
7220
7938
  explain(value, options = {}) {
7221
- return (0, import_resolution9.explainResolvedKey)({
7939
+ return (0, import_resolution10.explainResolvedKey)({
7222
7940
  db: this.db,
7223
7941
  rules: WITNESS_RULES,
7224
7942
  key: askedKey(value),
@@ -7359,6 +8077,8 @@ async function rubyStubEvidence(options) {
7359
8077
  resolveConstantFile,
7360
8078
  rubyStubEvidence,
7361
8079
  runStatements,
8080
+ storageClaims,
8081
+ storageEffects,
7362
8082
  stringLiteralValue,
7363
8083
  stringValueOf,
7364
8084
  symbolValue,