@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.js CHANGED
@@ -62,7 +62,25 @@ var OWN_BODY_TYPES = /* @__PURE__ */ new Set([
62
62
  "module",
63
63
  "singleton_class"
64
64
  ]);
65
+ var PROGRAM_TYPE = "program";
66
+ var MODULE_SCOPE_STOPS = /* @__PURE__ */ new Set([
67
+ ...OWN_BODY_TYPES,
68
+ "block",
69
+ "do_block"
70
+ ]);
65
71
  var NESTING_TYPES = /* @__PURE__ */ new Set(["class", "module"]);
72
+ var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
73
+ var LAMBDA_TYPE = "lambda";
74
+ function enclosingDefinition(node) {
75
+ let current = node.parent;
76
+ while (current !== null) {
77
+ if (METHOD_TYPES.has(current.type) || current.type === LAMBDA_TYPE) {
78
+ return current;
79
+ }
80
+ current = current.parent;
81
+ }
82
+ return null;
83
+ }
66
84
  var INCLUDE_CALL = "include";
67
85
  var PREPEND_CALL = "prepend";
68
86
  var ARGUMENT_LIST_TYPE = "argument_list";
@@ -330,7 +348,7 @@ import { nodeOfKey as nodeOfKey2 } from "@suss/resolution";
330
348
  import { hole as hole2, literalOf as literalOf3, piecesOf } from "@suss/values";
331
349
 
332
350
  // src/values/evaluator.ts
333
- import { nodeOfKey } from "@suss/resolution";
351
+ import { nodeOfKey, writtenValuesByKey } from "@suss/resolution";
334
352
  import { Evaluator, force, literalOf, text } from "@suss/values";
335
353
 
336
354
  // src/facts/resolve.ts
@@ -390,8 +408,14 @@ var RUBY_PROGRAM = resolutionProgram(RUBY_RULES);
390
408
  function resolveValues(db, keys) {
391
409
  askResolution(db, keys, "wanted", RUBY_PROGRAM);
392
410
  }
411
+ function resolveEnvObjects(db, objects) {
412
+ askResolution(db, objects, "wantedEnvObject", RUBY_PROGRAM);
413
+ }
393
414
  function resolvedFunctions(db, key) {
394
- return db.facts("wantedResolves").filter((row) => String(row[0]) === key).map((row) => String(row[1]));
415
+ return [
416
+ ...db.lookup("wantedResolves", 0, key),
417
+ ...db.lookup("wantedGivesBack", 0, key)
418
+ ].map((row) => String(row[1]));
395
419
  }
396
420
  function writtenValueOf(db, key) {
397
421
  resolveValues(db, [key]);
@@ -491,12 +515,15 @@ var BINDS_A_NAME = {
491
515
  PARAMETER_LIST_TYPES.map((type) => [type, parameterNames])
492
516
  )
493
517
  };
494
- function localNamesIn(definitionNode) {
518
+ var DESCENDS_EVERYWHERE = /* @__PURE__ */ new Set();
519
+ function localNamesIn(definitionNode, stops = DESCENDS_EVERYWHERE) {
495
520
  const names = /* @__PURE__ */ new Set();
496
521
  const visit = (node) => {
497
522
  BINDS_A_NAME[node.type]?.(node, names);
498
523
  for (const child of bodyStatements(node)) {
499
- visit(child);
524
+ if (!stops.has(child.type)) {
525
+ visit(child);
526
+ }
500
527
  }
501
528
  };
502
529
  visit(definitionNode);
@@ -509,11 +536,12 @@ function spellsAName(node) {
509
536
  }
510
537
  return SPELLS_A_NAME[parent.type]?.(parent, node) === true;
511
538
  }
539
+ var KEYWORD_LITERALS = /* @__PURE__ */ new Set(["__FILE__", "__LINE__", "__ENCODING__"]);
512
540
  function isBareMethodCall(node, locals) {
513
541
  if (node.type !== "identifier" || locals.has(node.text)) {
514
542
  return false;
515
543
  }
516
- return !spellsAName(node);
544
+ return !KEYWORD_LITERALS.has(node.text) && !spellsAName(node);
517
545
  }
518
546
 
519
547
  // src/facts/locals.ts
@@ -664,25 +692,46 @@ function namesOfFile(tree) {
664
692
  cache.file ??= scopeNames(/* @__PURE__ */ new Set(), tree.rootNode);
665
693
  return cache.file;
666
694
  }
695
+ function scopesAround(enclosing) {
696
+ const chain = [];
697
+ let current = enclosing;
698
+ while (current !== null && current.type === LAMBDA_TYPE) {
699
+ chain.push(current);
700
+ current = enclosingDefinition(current);
701
+ }
702
+ chain.push(current);
703
+ return chain;
704
+ }
705
+ var namesOfScope = (scope, node) => scope === null ? namesOfFile(node.tree) : namesOfMethod(scope);
667
706
  function ownerOfName(node, name, enclosing) {
668
707
  if (node.type === "constant") {
669
708
  return null;
670
709
  }
671
- const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
672
- if (names.blockParams.has(name)) {
673
- const block = blockDeclaring(node, name);
674
- if (block !== null) {
675
- return block;
710
+ for (const scope of scopesAround(enclosing)) {
711
+ const names = namesOfScope(scope, node);
712
+ if (names.blockParams.has(name)) {
713
+ const block = blockDeclaring(node, name);
714
+ if (block !== null) {
715
+ return block;
716
+ }
717
+ }
718
+ if (scope !== null && names.locals.has(name)) {
719
+ return scope;
676
720
  }
677
721
  }
678
- return enclosing !== null && names.locals.has(name) ? enclosing : null;
722
+ return null;
679
723
  }
680
724
  function isLocalName(node, name, enclosing) {
681
- const names = enclosing === null ? namesOfFile(node.tree) : namesOfMethod(enclosing);
682
- if (names.blockParams.has(name) && blockDeclaring(node, name) !== null) {
683
- return true;
725
+ for (const scope of scopesAround(enclosing)) {
726
+ const names = namesOfScope(scope, node);
727
+ if (names.blockParams.has(name) && blockDeclaring(node, name) !== null) {
728
+ return true;
729
+ }
730
+ if (names.locals.has(name)) {
731
+ return true;
732
+ }
684
733
  }
685
- return names.locals.has(name);
734
+ return false;
686
735
  }
687
736
  function blockDeclaring(node, name) {
688
737
  let current = node.parent;
@@ -873,19 +922,33 @@ function calleeKeyOf(filePath, node, enclosing) {
873
922
  }
874
923
  return field(node, "receiver") === null ? nameKey(filePath, method, enclosing) : nodeId(filePath, method);
875
924
  }
925
+ function invokedKeyOf(filePath, node, enclosing) {
926
+ const invoked = invokedValueOf(node);
927
+ return invoked === null ? null : readKey(filePath, invoked, enclosing);
928
+ }
876
929
  function emitCall(emitter, call) {
877
- add(emitter, "writtenValue", nodeId(emitter.filePath, call));
930
+ const callKey = nodeId(emitter.filePath, call);
931
+ add(emitter, "writtenValue", callKey);
932
+ const invoked = invokedValueOf(call);
933
+ if (invoked !== null) {
934
+ add(emitter, "call", callKey, valueKey(emitter, invoked));
935
+ }
936
+ if (!emitMessageSent(emitter, call, callKey) && invoked === null) {
937
+ return;
938
+ }
939
+ emitCallArguments(emitter, call, callKey);
940
+ }
941
+ function emitMessageSent(emitter, call, callKey) {
878
942
  const method = call.type === "identifier" ? call : field(call, "method");
879
943
  const calleeKey = calleeKeyOf(emitter.filePath, call, emitter.enclosing);
880
944
  if (method === null || calleeKey === null) {
881
- return;
945
+ return false;
882
946
  }
883
- const receiver = field(call, "receiver");
884
- const callKey = nodeId(emitter.filePath, call);
885
947
  add(emitter, "call", callKey, calleeKey);
886
948
  if (!emitter.insideMethod) {
887
949
  add(emitter, "callOutsideMethod", callKey);
888
950
  }
951
+ const receiver = field(call, "receiver");
889
952
  if (receiver !== null) {
890
953
  add(
891
954
  emitter,
@@ -897,6 +960,21 @@ function emitCall(emitter, call) {
897
960
  } else if (emitter.selfKey !== null) {
898
961
  add(emitter, "readsProperty", calleeKey, emitter.selfKey, method.text);
899
962
  }
963
+ return true;
964
+ }
965
+ var INVOKE_METHOD = "call";
966
+ function invokedValueOf(call) {
967
+ if (call.type !== "call") {
968
+ return null;
969
+ }
970
+ const receiver = field(call, "receiver");
971
+ if (receiver === null) {
972
+ return null;
973
+ }
974
+ const method = field(call, "method");
975
+ return method === null || method.text === INVOKE_METHOD ? receiver : null;
976
+ }
977
+ function emitCallArguments(emitter, call, callKey) {
900
978
  const args = field(call, "arguments");
901
979
  let position = 0;
902
980
  for (const argument of args === null ? [] : children(args)) {
@@ -1016,12 +1094,58 @@ function walkExpressions(node, emitter, visit, turns = []) {
1016
1094
  );
1017
1095
  }
1018
1096
  }
1097
+ var WRITTEN_KEY_TYPES = /* @__PURE__ */ new Set([
1098
+ "string",
1099
+ "integer",
1100
+ "simple_symbol",
1101
+ "bare_string",
1102
+ "bare_symbol"
1103
+ ]);
1104
+ function emitKeyedRead(emitter, site, container, key) {
1105
+ if (WRITTEN_KEY_TYPES.has(key.type)) {
1106
+ return;
1107
+ }
1108
+ add(
1109
+ emitter,
1110
+ "readsKeyed",
1111
+ nodeId(emitter.filePath, site),
1112
+ valueKey(emitter, container),
1113
+ valueKey(emitter, key)
1114
+ );
1115
+ }
1116
+ function emitKeyedElement(emitter, node) {
1117
+ const object = field(node, "object");
1118
+ if (object === null || isWriteTarget(node)) {
1119
+ return;
1120
+ }
1121
+ const index = node.namedChildren.find(
1122
+ (child) => child !== null && child.id !== object.id
1123
+ );
1124
+ if (index !== void 0) {
1125
+ emitKeyedRead(emitter, node, object, index);
1126
+ }
1127
+ }
1128
+ function emitKeyedFetch(emitter, call) {
1129
+ const receiver = field(call, "receiver");
1130
+ if (receiver === null || field(call, "method")?.text !== "fetch") {
1131
+ return;
1132
+ }
1133
+ const { positional } = readCallArgs(field(call, "arguments"));
1134
+ const key = positional[0];
1135
+ if (key !== void 0) {
1136
+ emitKeyedRead(emitter, call, receiver, key);
1137
+ }
1138
+ }
1019
1139
  function emitExpressionFacts(emitter, node) {
1020
1140
  emitDynamicDefinition(emitter, node, []);
1021
1141
  walkExpressions(node, emitter, (child, turns) => {
1022
1142
  emitDynamicDefinition(emitter, child, turns);
1023
1143
  if (child.type === "call" || isBareCall(child, emitter.enclosing)) {
1024
1144
  emitCall(emitter, child);
1145
+ emitKeyedFetch(emitter, child);
1146
+ }
1147
+ if (child.type === "element_reference") {
1148
+ emitKeyedElement(emitter, child);
1025
1149
  }
1026
1150
  if (ARRAY_TYPES.has(child.type)) {
1027
1151
  emitArray(emitter, child);
@@ -1145,6 +1269,25 @@ function assignedValueOf(node) {
1145
1269
  }
1146
1270
  return field(node, "right");
1147
1271
  }
1272
+ function emitLambdasIn(emitter, body) {
1273
+ for (const child of children(body)) {
1274
+ if (child.type === LAMBDA_TYPE) {
1275
+ emitMethodFacts(emitter, child);
1276
+ continue;
1277
+ }
1278
+ if (OWN_BODY_TYPES.has(child.type)) {
1279
+ continue;
1280
+ }
1281
+ emitLambdasIn(emitter, child);
1282
+ }
1283
+ }
1284
+ function definitionBody(definition) {
1285
+ const body = field(definition, "body");
1286
+ if (body === null || definition.type !== LAMBDA_TYPE) {
1287
+ return body;
1288
+ }
1289
+ return field(body, "body");
1290
+ }
1148
1291
  function emitMethodFacts(emitter, method) {
1149
1292
  const funcKey = nodeId(emitter.filePath, method);
1150
1293
  add(emitter, "func", funcKey);
@@ -1159,7 +1302,7 @@ function emitMethodFacts(emitter, method) {
1159
1302
  position += 1;
1160
1303
  }
1161
1304
  const inside = { ...emitter, enclosing: method };
1162
- const body = field(method, "body");
1305
+ const body = definitionBody(method);
1163
1306
  if (body === null) {
1164
1307
  return funcKey;
1165
1308
  }
@@ -1191,6 +1334,7 @@ function emitMethodFacts(emitter, method) {
1191
1334
  }
1192
1335
  emitExpressionFacts(inside, body);
1193
1336
  emitScopeWrites(inside, method, body);
1337
+ emitLambdasIn(inside, body);
1194
1338
  return funcKey;
1195
1339
  }
1196
1340
  function isConstruction(value) {
@@ -1270,7 +1414,6 @@ function emitCandidates(emitter, key, group) {
1270
1414
  add(emitter, "writesAllStated", key);
1271
1415
  }
1272
1416
  }
1273
- var METHOD_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method"]);
1274
1417
  var INITIALIZE_METHOD = "initialize";
1275
1418
  function receiverKeyOf(filePath, method, classKey) {
1276
1419
  return method.type === "singleton_method" ? classKey : `${nodeId(filePath, method)}#self`;
@@ -1318,7 +1461,7 @@ function emitClassFacts(emitter, cls) {
1318
1461
  left.text,
1319
1462
  valueKey(emitter, right)
1320
1463
  );
1321
- emitExpressionFacts(emitter, statement);
1464
+ emitExpressionFacts(within, statement);
1322
1465
  }
1323
1466
  continue;
1324
1467
  }
@@ -1351,12 +1494,15 @@ function emitClassFacts(emitter, cls) {
1351
1494
  add(emitter, "initializes", classKey, funcKey);
1352
1495
  }
1353
1496
  }
1354
- const methodBody = field(statement, "body");
1355
- if (methodBody !== null) {
1356
- emitInstanceStores(within, funcKey, methodBody, stored);
1497
+ const methodBody2 = field(statement, "body");
1498
+ if (methodBody2 !== null) {
1499
+ emitInstanceStores(within, funcKey, methodBody2, stored);
1357
1500
  }
1358
1501
  }
1359
1502
  emitInstanceWrites(within, classKey, collected);
1503
+ if (body !== null) {
1504
+ emitLambdasIn(within, body);
1505
+ }
1360
1506
  return classKey;
1361
1507
  }
1362
1508
  function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
@@ -1392,6 +1538,7 @@ function emitValueFacts(db, filePath, root, bodyBlocks = NO_BODY_BLOCKS) {
1392
1538
  walk(root);
1393
1539
  emitExpressionFacts(emitter, root);
1394
1540
  emitScopeWrites(emitter, null, root);
1541
+ emitLambdasIn(emitter, root);
1395
1542
  }
1396
1543
 
1397
1544
  // src/values/lowering.ts
@@ -2399,7 +2546,6 @@ var evaluators = /* @__PURE__ */ new WeakMap();
2399
2546
  var contexts = /* @__PURE__ */ new WeakMap();
2400
2547
  var withoutFacts = /* @__PURE__ */ new WeakMap();
2401
2548
  var filesByTree = /* @__PURE__ */ new WeakMap();
2402
- var METHOD_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
2403
2549
  function bindEvaluator(db, nodes) {
2404
2550
  for (const entry of nodes.files) {
2405
2551
  filesByTree.set(entry.root.tree, entry.file);
@@ -2412,6 +2558,24 @@ function writtenNodeOf(node, db, site) {
2412
2558
  const context = db === void 0 ? void 0 : contexts.get(db);
2413
2559
  return context === void 0 ? null : context.writtenTo(node, site);
2414
2560
  }
2561
+ function askWrittenValues(nodes, db) {
2562
+ if (db === void 0 || !contexts.has(db)) {
2563
+ return;
2564
+ }
2565
+ const keys = /* @__PURE__ */ new Set();
2566
+ for (const node of nodes) {
2567
+ const file = filesByTree.get(node.tree);
2568
+ if (file !== void 0) {
2569
+ keys.add(readKey(file, node, enclosingDefinition(node)));
2570
+ }
2571
+ }
2572
+ if (keys.size === 0) {
2573
+ return;
2574
+ }
2575
+ const asked = [...keys];
2576
+ resolveValues(db, asked);
2577
+ writtenValuesByKey(db, asked, (more) => resolveValues(db, more));
2578
+ }
2415
2579
  function evaluatedValue(node, db, bindings, site) {
2416
2580
  const evaluator = evaluatorFor(node, db);
2417
2581
  if (bindings === void 0) {
@@ -2453,7 +2617,7 @@ function stringValueOf(node, db, bindings) {
2453
2617
  function methodDefinitionsIn(file, root) {
2454
2618
  const found = /* @__PURE__ */ new Map();
2455
2619
  const visit = (node) => {
2456
- if (METHOD_TYPES3.has(node.type)) {
2620
+ if (METHOD_TYPES.has(node.type)) {
2457
2621
  found.set(nodeId(file, node), node);
2458
2622
  }
2459
2623
  for (const child of node.namedChildren) {
@@ -2478,16 +2642,6 @@ function evaluatorFor(node, db) {
2478
2642
  }
2479
2643
  return local;
2480
2644
  }
2481
- function enclosingMethod(node) {
2482
- let current = node.parent;
2483
- while (current !== null) {
2484
- if (METHOD_TYPES3.has(current.type)) {
2485
- return current;
2486
- }
2487
- current = current.parent;
2488
- }
2489
- return null;
2490
- }
2491
2645
  function contextOver(db, nodes) {
2492
2646
  const filesByRoot = /* @__PURE__ */ new Map();
2493
2647
  const rootsByFile = /* @__PURE__ */ new Map();
@@ -2502,7 +2656,7 @@ function contextOver(db, nodes) {
2502
2656
  if (file === null) {
2503
2657
  return null;
2504
2658
  }
2505
- const key = readKey(file, node, enclosingMethod(node));
2659
+ const key = readKey(file, node, enclosingDefinition(node));
2506
2660
  const answer = site === void 0 ? writtenValueOf(db, key) : writtenValueUnder(db, key, site);
2507
2661
  return answer === null ? null : nodeOfKey(rootsByFile, answer);
2508
2662
  },
@@ -2512,7 +2666,7 @@ function contextOver(db, nodes) {
2512
2666
  if (file === null || method === null) {
2513
2667
  return null;
2514
2668
  }
2515
- const key = field(call, "receiver") === null ? readKey(file, method, enclosingMethod(call)) : nodeId(file, method);
2669
+ const key = field(call, "receiver") === null ? readKey(file, method, enclosingDefinition(call)) : nodeId(file, method);
2516
2670
  resolveValues(db, [key]);
2517
2671
  const resolved = resolvedFunctions(db, key);
2518
2672
  const settled = resolved.length === 1 ? resolved[0] : void 0;
@@ -2664,8 +2818,8 @@ function qualifyConstantRef(node, nesting) {
2664
2818
  return compoundName(node);
2665
2819
  }
2666
2820
  if (node.type === "constant") {
2667
- const innermost = nesting[0] ?? null;
2668
- return innermost === null ? node.text : `${innermost}::${node.text}`;
2821
+ const innermost2 = nesting[0] ?? null;
2822
+ return innermost2 === null ? node.text : `${innermost2}::${node.text}`;
2669
2823
  }
2670
2824
  return null;
2671
2825
  }
@@ -2722,9 +2876,9 @@ function ownIdentity(nameNode, outerNesting) {
2722
2876
  return { qualifiedName, bodyNesting: [qualifiedName, ...outerNesting] };
2723
2877
  }
2724
2878
  function definedName(nameNode, outerNesting) {
2725
- const innermost = outerNesting[0] ?? null;
2726
- if (nameNode.type === "scope_resolution" && innermost !== null && !nameNode.text.startsWith("::")) {
2727
- return `${innermost}::${nameNode.text}`;
2879
+ const innermost2 = outerNesting[0] ?? null;
2880
+ if (nameNode.type === "scope_resolution" && innermost2 !== null && !nameNode.text.startsWith("::")) {
2881
+ return `${innermost2}::${nameNode.text}`;
2728
2882
  }
2729
2883
  return qualifyConstantRef(nameNode, outerNesting);
2730
2884
  }
@@ -3352,24 +3506,38 @@ function isCall(node, locals, inherited) {
3352
3506
  }
3353
3507
  return isBareMethodCall(node, locals) && !RAISE_NAMES2.has(node.text) && !inherited.has(node.text);
3354
3508
  }
3355
- function collectCalls(node, locals, inherited, found) {
3509
+ function collectCalls(node, read, inherited, found) {
3356
3510
  for (const child of children2(node)) {
3357
- if (OWN_BODY_TYPES.has(child.type)) {
3511
+ if (read.stops.has(child.type)) {
3358
3512
  continue;
3359
3513
  }
3360
- if (isCall(child, locals, inherited)) {
3514
+ if (isCall(child, read.locals, inherited)) {
3361
3515
  found.push(child);
3362
3516
  }
3363
- collectCalls(child, locals, inherited, found);
3517
+ collectCalls(child, read, inherited, found);
3364
3518
  }
3365
3519
  return found;
3366
3520
  }
3367
- function bodyCalls(definitionNode, inherited = NO_INHERITED_METHODS) {
3521
+ function methodBody(definitionNode) {
3368
3522
  const body = field(definitionNode, "body");
3369
3523
  if (body === null) {
3370
- return [];
3524
+ return null;
3371
3525
  }
3372
- return collectCalls(body, localNamesIn(definitionNode), inherited, []);
3526
+ return {
3527
+ body,
3528
+ locals: localNamesIn(definitionNode),
3529
+ stops: OWN_BODY_TYPES
3530
+ };
3531
+ }
3532
+ function moduleScopeBody(root) {
3533
+ return {
3534
+ body: root,
3535
+ locals: localNamesIn(root, MODULE_SCOPE_STOPS),
3536
+ stops: MODULE_SCOPE_STOPS
3537
+ };
3538
+ }
3539
+ function bodyCalls(read, inherited = NO_INHERITED_METHODS) {
3540
+ return collectCalls(read.body, read, inherited, []);
3373
3541
  }
3374
3542
  function calleeText(call) {
3375
3543
  const receiver = field(call, "receiver");
@@ -3413,14 +3581,23 @@ function enclosingStatement(call, body) {
3413
3581
  return current;
3414
3582
  }
3415
3583
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
3416
- const body = field(definitionNode, "body");
3417
- if (body === null) {
3584
+ const read = methodBody(definitionNode);
3585
+ if (read === null) {
3418
3586
  return [];
3419
3587
  }
3420
- const calls = callsReported(
3421
- bodyCalls(definitionNode, inherited),
3422
- keepsArglessCall
3588
+ return effectsOfBody(read, inherited, keepsArglessCall, facts);
3589
+ }
3590
+ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
3591
+ return effectsOfBody(
3592
+ moduleScopeBody(root),
3593
+ inherited,
3594
+ keepsArglessCall,
3595
+ facts
3423
3596
  );
3597
+ }
3598
+ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
3599
+ const body = read.body;
3600
+ const calls = callsReported(bodyCalls(read, inherited), keepsArglessCall);
3424
3601
  if (calls.length === 0) {
3425
3602
  return [];
3426
3603
  }
@@ -3832,6 +4009,57 @@ function clientCallUnits(root, pack, pattern, options) {
3832
4009
  }
3833
4010
  return units;
3834
4011
  }
4012
+ function askClientCallReads(root, patterns, options) {
4013
+ const facts = options.facts;
4014
+ if (facts === void 0 || patterns.length === 0) {
4015
+ return;
4016
+ }
4017
+ const calls = [];
4018
+ for (const method of methodDefinitions(root)) {
4019
+ calls.push(...callsUnder(method));
4020
+ }
4021
+ if (patterns.some((pattern) => (pattern.receiverBuilders ?? []).length > 0)) {
4022
+ askWrittenValues(receiversOf(calls), facts);
4023
+ }
4024
+ askWrittenValues(requestArguments(calls, patterns, options), facts);
4025
+ }
4026
+ function receiversOf(calls) {
4027
+ const found = [];
4028
+ for (const call of calls) {
4029
+ const receiver = field(call, "receiver");
4030
+ if (receiver !== null) {
4031
+ found.push(receiver);
4032
+ }
4033
+ }
4034
+ return found;
4035
+ }
4036
+ function requestArguments(calls, patterns, options) {
4037
+ const found = [];
4038
+ for (const call of calls) {
4039
+ const receiver = field(call, "receiver");
4040
+ const called = field(call, "method")?.text;
4041
+ if (receiver === null || called === void 0) {
4042
+ continue;
4043
+ }
4044
+ for (const pattern of patterns) {
4045
+ if (!isLibraryReceiver(receiver, pattern, options)) {
4046
+ continue;
4047
+ }
4048
+ const argument = requestArgument(call, called, pattern);
4049
+ if (argument !== void 0) {
4050
+ found.push(argument);
4051
+ }
4052
+ }
4053
+ }
4054
+ return found;
4055
+ }
4056
+ function requestArgument(call, called, pattern) {
4057
+ const args = readCallArgs(field(call, "arguments"));
4058
+ if (pattern.verbMethodNames[called] !== void 0) {
4059
+ return urlNodeIn(args, pattern);
4060
+ }
4061
+ return pattern.requestObject?.attribute === called ? args.positional[0] : void 0;
4062
+ }
3835
4063
  function methodDefinitions(node, found = []) {
3836
4064
  for (const child of node.namedChildren) {
3837
4065
  if (child === null) {
@@ -3956,9 +4184,11 @@ function builderCallBehind(receiver, pattern, facts) {
3956
4184
  return constant2 !== null && namesConstant(constant2, pattern.constantName) ? written : null;
3957
4185
  }
3958
4186
  function urlIn(args, pattern, options, site) {
4187
+ return pathAt(urlNodeIn(args, pattern), options, site);
4188
+ }
4189
+ function urlNodeIn(args, pattern) {
3959
4190
  const keyword = pattern.url.keyword;
3960
- const written = (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
3961
- return pathAt(written, options, site);
4191
+ return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[pattern.url.position];
3962
4192
  }
3963
4193
  function pathAt(written, options, site) {
3964
4194
  if (written === void 0) {
@@ -4035,19 +4265,161 @@ function returnBranch(range, effects) {
4035
4265
  import { runtimeConfigBinding } from "@suss/behavioral-ir";
4036
4266
  import { SKIP_CHILDREN as SKIP_CHILDREN2, walkDescendants as walkDescendants2 } from "@suss/extractor";
4037
4267
  var RUBY_ENV_RECOGNITION = "ruby-env";
4268
+ var ENV_DEFAULTED = "rbEnvDefaulted";
4038
4269
  var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
4039
- function envReadEffects(root) {
4040
- const effects = [];
4270
+ function envReadEffects(root, facts) {
4271
+ const helpers = helperFactsOf(facts);
4272
+ const slots = [];
4041
4273
  walkDescendants2(root, null, {
4042
4274
  at: (node) => {
4043
4275
  const read = envReadAt(node);
4044
4276
  if (read !== null) {
4045
- effects.push(configReadEffect(read));
4277
+ slots.push({ read });
4278
+ return;
4279
+ }
4280
+ if (helpers !== null && node.type === "call") {
4281
+ slots.push({ call: node });
4046
4282
  }
4047
4283
  },
4048
4284
  into: (node) => DEFERRED_BODY_TYPES.has(node.type) ? SKIP_CHILDREN2 : null
4049
4285
  });
4050
- return effects;
4286
+ const calls = slots.flatMap((slot) => "call" in slot ? [slot.call] : []);
4287
+ const throughHelpers = helpers === null ? new NodeMap() : helperReads(calls, helpers);
4288
+ return slots.flatMap((slot) => {
4289
+ if ("read" in slot) {
4290
+ return [configReadEffect(slot.read)];
4291
+ }
4292
+ return (throughHelpers.get(slot.call) ?? []).map(configReadEffect);
4293
+ });
4294
+ }
4295
+ function helperFactsOf(facts) {
4296
+ if (facts === void 0) {
4297
+ return null;
4298
+ }
4299
+ const named2 = namedParameters(facts.db);
4300
+ if (named2.size === 0) {
4301
+ return null;
4302
+ }
4303
+ return { ...facts, named: named2 };
4304
+ }
4305
+ var namedByDb = /* @__PURE__ */ new WeakMap();
4306
+ function namedParameters(db) {
4307
+ const memo = namedByDb.get(db);
4308
+ if (memo !== void 0) {
4309
+ return memo;
4310
+ }
4311
+ const named2 = /* @__PURE__ */ new Map();
4312
+ namedByDb.set(db, named2);
4313
+ const objects = db.facts("environmentObject").map((row) => String(row[0]));
4314
+ if (objects.length === 0) {
4315
+ return named2;
4316
+ }
4317
+ resolveEnvObjects(db, objects);
4318
+ for (const row of db.facts("wantedParamNamesEnv")) {
4319
+ const found = named2.get(String(row[0]));
4320
+ if (found === void 0) {
4321
+ named2.set(String(row[0]), [String(row[1])]);
4322
+ continue;
4323
+ }
4324
+ found.push(String(row[1]));
4325
+ }
4326
+ return named2;
4327
+ }
4328
+ function emitEnvFacts(db, file, root) {
4329
+ walkDescendants2(root, null, {
4330
+ at: (node) => {
4331
+ if (isEnv(node) && handsOnward(node)) {
4332
+ db.add("environmentObject", [
4333
+ readKey(file, node, enclosingDefinition(node))
4334
+ ]);
4335
+ }
4336
+ const site = envSiteAt(node);
4337
+ if (site !== null && site.defaulted && stringLiteralValue(site.name) === null) {
4338
+ db.add(ENV_DEFAULTED, [nodeId(file, site.read)]);
4339
+ }
4340
+ },
4341
+ into: () => null
4342
+ });
4343
+ }
4344
+ function handsOnward(node) {
4345
+ const parent = node.parent;
4346
+ if (parent === null) {
4347
+ return true;
4348
+ }
4349
+ if (parent.type === "element_reference" || parent.type === "call") {
4350
+ const site = envSiteAt(parent);
4351
+ return site === null || stringLiteralValue(site.name) === null;
4352
+ }
4353
+ return true;
4354
+ }
4355
+ function helperReads(calls, facts) {
4356
+ const { db, file, named: named2 } = facts;
4357
+ const callees = new NodeMap();
4358
+ for (const call of calls) {
4359
+ const enclosing = enclosingDefinition(call);
4360
+ const keys = [
4361
+ calleeKeyOf(file, call, enclosing),
4362
+ invokedKeyOf(file, call, enclosing)
4363
+ ].filter((key) => key !== null);
4364
+ if (keys.length > 0) {
4365
+ callees.set(call, keys);
4366
+ }
4367
+ }
4368
+ const reads = new NodeMap();
4369
+ if (callees.size === 0) {
4370
+ return reads;
4371
+ }
4372
+ resolveValues(
4373
+ db,
4374
+ [...callees].flatMap(([, keys]) => keys)
4375
+ );
4376
+ for (const [call, callee] of callees) {
4377
+ const args = namingArguments(db, named2, call, callee);
4378
+ if (args.length === 0) {
4379
+ continue;
4380
+ }
4381
+ const found = readsAtCall(db, call, args);
4382
+ if (found.length > 0) {
4383
+ reads.set(call, found);
4384
+ }
4385
+ }
4386
+ return reads;
4387
+ }
4388
+ function namingArguments(db, named2, call, callees) {
4389
+ const { positional, keyword } = readCallArgs(field(call, "arguments"));
4390
+ const found = [];
4391
+ for (const func of callees.flatMap((key) => resolvedFunctions(db, key))) {
4392
+ for (const row of db.lookup("paramOf", 0, func)) {
4393
+ const sites = named2.get(String(row[2]));
4394
+ const argument = positional[Number(row[1])];
4395
+ if (sites !== void 0 && argument !== void 0) {
4396
+ found.push({ sites, argument });
4397
+ }
4398
+ }
4399
+ for (const row of db.lookup("paramNamed", 0, func)) {
4400
+ const sites = named2.get(String(row[2]));
4401
+ const argument = keyword[String(row[1])];
4402
+ if (sites !== void 0 && argument !== void 0) {
4403
+ found.push({ sites, argument });
4404
+ }
4405
+ }
4406
+ }
4407
+ return found;
4408
+ }
4409
+ function readsAtCall(db, call, args) {
4410
+ const defaultedAtCall = isDefaultedAt(call);
4411
+ const byName = /* @__PURE__ */ new Map();
4412
+ for (const { sites, argument } of args) {
4413
+ const name = stringLiteralValue(argument) ?? stringValueOf(argument, db);
4414
+ if (name === null) {
4415
+ continue;
4416
+ }
4417
+ for (const site of sites) {
4418
+ const defaulted = defaultedAtCall || db.lookup(ENV_DEFAULTED, 0, site).length > 0;
4419
+ byName.set(name, (byName.get(name) ?? true) && defaulted);
4420
+ }
4421
+ }
4422
+ return [...byName].map(([name, defaulted]) => ({ name, defaulted }));
4051
4423
  }
4052
4424
  function configReadEffect(read) {
4053
4425
  return {
@@ -4066,15 +4438,23 @@ function configReadEffect(read) {
4066
4438
  };
4067
4439
  }
4068
4440
  function envReadAt(node) {
4441
+ const site = envSiteAt(node);
4442
+ if (site === null) {
4443
+ return null;
4444
+ }
4445
+ const name = stringLiteralValue(site.name);
4446
+ return name === null ? null : { name, defaulted: site.defaulted };
4447
+ }
4448
+ function envSiteAt(node) {
4069
4449
  if (node.type === "element_reference") {
4070
- return elementRead(node);
4450
+ return elementSite(node);
4071
4451
  }
4072
4452
  if (node.type === "call") {
4073
- return fetchRead(node);
4453
+ return fetchSite(node);
4074
4454
  }
4075
4455
  return null;
4076
4456
  }
4077
- function elementRead(node) {
4457
+ function elementSite(node) {
4078
4458
  const object = field(node, "object");
4079
4459
  if (object === null || !isEnv(object) || isAssignedTo(node)) {
4080
4460
  return null;
@@ -4082,24 +4462,23 @@ function elementRead(node) {
4082
4462
  const index = node.namedChildren.find(
4083
4463
  (child) => child !== null && child.id !== object.id
4084
4464
  );
4085
- const name = index === void 0 ? null : stringLiteralValue(index);
4086
- if (name === null) {
4465
+ if (index === void 0) {
4087
4466
  return null;
4088
4467
  }
4089
- return { name, defaulted: isDefaultedAt(node) };
4468
+ return { read: node, name: index, defaulted: isDefaultedAt(node) };
4090
4469
  }
4091
- function fetchRead(node) {
4470
+ function fetchSite(node) {
4092
4471
  const receiver = field(node, "receiver");
4093
4472
  if (receiver === null || !isEnv(receiver) || field(node, "method")?.text !== "fetch") {
4094
4473
  return null;
4095
4474
  }
4096
4475
  const { positional } = readCallArgs(field(node, "arguments"));
4097
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
4098
- if (name === null) {
4476
+ const name = positional[0];
4477
+ if (name === void 0) {
4099
4478
  return null;
4100
4479
  }
4101
4480
  const hasDefault = positional.length > 1 || field(node, "block") !== null;
4102
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
4481
+ return { read: node, name, defaulted: hasDefault || isDefaultedAt(node) };
4103
4482
  }
4104
4483
  function isEnv(node) {
4105
4484
  if (node.type === "constant") {
@@ -4273,6 +4652,9 @@ function methodNamesOf(args, filter, facts) {
4273
4652
  const value = named2 === void 0 ? null : stringValueOf(named2, facts);
4274
4653
  return value === null ? [] : [value];
4275
4654
  }
4655
+ return symbolArgumentNames(args, facts);
4656
+ }
4657
+ function symbolArgumentNames(args, facts) {
4276
4658
  return args.positional.map((arg) => stringValueOf(arg, facts)).filter((name) => name !== null);
4277
4659
  }
4278
4660
  function skipsOf(statement, filterName, facts) {
@@ -4311,19 +4693,315 @@ function actionsUnder(node, facts) {
4311
4693
  }
4312
4694
 
4313
4695
  // src/storage.ts
4314
- import { storageBinding } from "@suss/ir-core";
4696
+ import { storageBinding as storageBinding2 } from "@suss/ir-core";
4697
+ import { askResolution as askResolution3 } from "@suss/resolution";
4698
+
4699
+ // src/baseClass.ts
4315
4700
  import { askResolution as askResolution2 } from "@suss/resolution";
4316
- function children3(node) {
4317
- return node.namedChildren.filter((child) => child !== null);
4701
+ function classBehind(facts, file, constant2) {
4702
+ const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4703
+ const bound = new Set(
4704
+ facts.lookup("binds", 0, key).map((row) => String(row[1]))
4705
+ );
4706
+ return bound.size === 1 ? [...bound][0] : void 0;
4318
4707
  }
4708
+ function reachesBase(facts, classKey, bases) {
4709
+ askResolution2(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4710
+ return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4711
+ }
4712
+ function enclosingClassKey(node, file) {
4713
+ let current = node.parent;
4714
+ while (current !== null) {
4715
+ if (NESTING_TYPES.has(current.type)) {
4716
+ return nodeId(file, current);
4717
+ }
4718
+ current = current.parent;
4719
+ }
4720
+ return null;
4721
+ }
4722
+
4723
+ // src/loaders.ts
4319
4724
  function receiverOf(node) {
4320
4725
  return node.type === "call" ? field(node, "receiver") : null;
4321
4726
  }
4322
4727
  function methodOf(node) {
4323
4728
  return field(node, "method")?.text ?? "";
4324
4729
  }
4730
+ function isLoader(node, loader) {
4731
+ if (node === null) {
4732
+ return false;
4733
+ }
4734
+ if (node.type === "identifier") {
4735
+ return node.text === loader.loader;
4736
+ }
4737
+ return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
4738
+ }
4739
+ function loaderPick(call, loader) {
4740
+ const method = methodOf(call);
4741
+ const receiver = receiverOf(call);
4742
+ if (receiver === null) {
4743
+ return loader.shortcuts.includes(method) ? call : null;
4744
+ }
4745
+ if (!loader.reads.includes(method) || receiver.type !== "call") {
4746
+ return null;
4747
+ }
4748
+ const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
4749
+ return picks ? receiver : null;
4750
+ }
4751
+ function pickedSource(call, loader) {
4752
+ const source = loader.source;
4753
+ const pick = source === void 0 ? null : loaderPick(call, loader);
4754
+ if (source === void 0 || pick === null || methodOf(pick) !== loader.pick) {
4755
+ return null;
4756
+ }
4757
+ const args = field(pick, "arguments");
4758
+ const given = args === null ? void 0 : args.namedChildren[source.at];
4759
+ return given?.type === "constant" || given?.type === "scope_resolution" ? given : null;
4760
+ }
4761
+
4762
+ // src/rawSql.ts
4763
+ import { storageBinding } from "@suss/ir-core";
4764
+ import { readSqlAccess, splitQualifiedTable, sqlFromParts } from "@suss/sql";
4765
+ import { piecesOf as piecesOf2 } from "@suss/values";
4766
+ var MOST_HOPS = 8;
4767
+ var NOWHERE = { scope: null, container: null };
4768
+ function rawSqlEffects(call, options) {
4769
+ for (const pattern of options.patterns) {
4770
+ const effects = patternEffects(call, pattern, options);
4771
+ if (effects.length > 0) {
4772
+ return effects;
4773
+ }
4774
+ }
4775
+ return [];
4776
+ }
4777
+ function patternEffects(call, pattern, options) {
4778
+ const receiver = field(call, "receiver");
4779
+ if (receiver === null) {
4780
+ return [];
4781
+ }
4782
+ const address = addressBehind(receiver, pattern, options, 0);
4783
+ if (address === null) {
4784
+ return [];
4785
+ }
4786
+ const method = field(call, "method")?.text ?? "";
4787
+ const args = readCallArgs(field(call, "arguments"));
4788
+ const place = pattern.statements?.[method];
4789
+ if (place !== void 0) {
4790
+ const statement = statementAt(
4791
+ args,
4792
+ place,
4793
+ options.facts,
4794
+ pattern.bindPlaceholder
4795
+ );
4796
+ return statementEffects(call, statement, address, pattern);
4797
+ }
4798
+ return rowCallEffects(call, args, address, pattern, options);
4799
+ }
4800
+ function statementEffects(call, statement, address, store) {
4801
+ if (statement === null) {
4802
+ return [];
4803
+ }
4804
+ return readSqlAccess(statement, { dialect: store.dialect }).map(
4805
+ (access) => effectOf(
4806
+ call,
4807
+ store,
4808
+ {
4809
+ scope: innermost(access.qualifier) ?? address.scope,
4810
+ container: access.table
4811
+ },
4812
+ {
4813
+ kind: access.kind,
4814
+ fields: access.fields,
4815
+ selector: access.selector
4816
+ }
4817
+ )
4818
+ );
4819
+ }
4820
+ function innermost(qualifier) {
4821
+ return qualifier[qualifier.length - 1] ?? null;
4822
+ }
4823
+ function rowCallEffects(call, args, address, pattern, options) {
4824
+ const method = field(call, "method")?.text ?? "";
4825
+ const rowCall = pattern.rowCalls?.[method];
4826
+ if (rowCall === void 0) {
4827
+ return [];
4828
+ }
4829
+ const given = rowCall.container === void 0 ? null : namedTable(stringAt(args, rowCall.container, options.facts));
4830
+ const reached = {
4831
+ scope: given?.scope ?? address.scope,
4832
+ container: given?.container ?? address.container
4833
+ };
4834
+ if (reached.container === null) {
4835
+ return [];
4836
+ }
4837
+ return [
4838
+ effectOf(call, pattern, reached, {
4839
+ kind: rowCall.kind,
4840
+ fields: [],
4841
+ selector: []
4842
+ })
4843
+ ];
4844
+ }
4845
+ function namedTable(name) {
4846
+ const split = name === null ? null : splitQualifiedTable(name);
4847
+ return split === null ? null : { scope: innermost(split.qualifier), container: split.table };
4848
+ }
4849
+ function effectOf(call, store, address, access) {
4850
+ const operation = field(call, "method")?.text ?? "";
4851
+ return {
4852
+ type: "interaction",
4853
+ binding: storageBinding({
4854
+ recognition: "ruby-raw-sql",
4855
+ storageSystem: store.storageSystem,
4856
+ scope: address.scope ?? store.scope ?? "default",
4857
+ container: address.container
4858
+ }),
4859
+ callee: call.text,
4860
+ interaction: {
4861
+ class: "storage-access",
4862
+ kind: access.kind,
4863
+ fields: access.fields,
4864
+ operation,
4865
+ ...access.selector.length > 0 ? { selector: access.selector } : {}
4866
+ }
4867
+ };
4868
+ }
4869
+ function addressBehind(receiver, pattern, options, hops) {
4870
+ if (hops > MOST_HOPS) {
4871
+ return null;
4872
+ }
4873
+ if (receiver.type !== "call") {
4874
+ const written = writtenNodeOf(receiver, options.facts);
4875
+ if (written !== null && written.id !== receiver.id) {
4876
+ return addressBehind(written, pattern, options, hops + 1);
4877
+ }
4878
+ return receiver.type === "identifier" && buildsOnOwnClass(receiver, receiver.text, pattern, options) ? NOWHERE : null;
4879
+ }
4880
+ const method = field(receiver, "method")?.text ?? "";
4881
+ const inner = field(receiver, "receiver");
4882
+ if (inner === null) {
4883
+ return buildsOnOwnClass(receiver, method, pattern, options) ? NOWHERE : null;
4884
+ }
4885
+ if (namesConstant2(inner, pattern.constantName) || namesSubclass(inner, pattern, options)) {
4886
+ return pattern.clientBuilders.includes(method) ? NOWHERE : null;
4887
+ }
4888
+ const addressing = pattern.addressing?.[method];
4889
+ if (addressing === void 0 || inner === null) {
4890
+ return null;
4891
+ }
4892
+ const behind = addressBehind(inner, pattern, options, hops + 1);
4893
+ if (behind === null) {
4894
+ return null;
4895
+ }
4896
+ const named2 = stringAt(
4897
+ readCallArgs(field(receiver, "arguments")),
4898
+ addressing,
4899
+ options.facts
4900
+ );
4901
+ if (addressing.says === "scope") {
4902
+ return { scope: named2 ?? behind.scope, container: behind.container };
4903
+ }
4904
+ const table = namedTable(named2);
4905
+ return {
4906
+ scope: table?.scope ?? behind.scope,
4907
+ container: table?.container ?? behind.container
4908
+ };
4909
+ }
4910
+ function namesConstant2(receiver, constantName2) {
4911
+ if (receiver.type === "constant") {
4912
+ return receiver.text === constantName2;
4913
+ }
4914
+ return receiver.type === "scope_resolution" && compoundName(receiver) === constantName2;
4915
+ }
4916
+ function namesSubclass(receiver, pattern, options) {
4917
+ const bases = pattern.baseClasses ?? [];
4918
+ const facts = options.facts;
4919
+ const file = options.file;
4920
+ if (bases.length === 0 || facts === void 0 || file === void 0 || receiver.type !== "constant" && receiver.type !== "scope_resolution") {
4921
+ return false;
4922
+ }
4923
+ const classKey = classBehind(facts, file, receiver);
4924
+ return classKey !== void 0 && reachesBase(facts, classKey, bases);
4925
+ }
4926
+ function buildsOnOwnClass(node, method, pattern, options) {
4927
+ const bases = pattern.baseClasses ?? [];
4928
+ const facts = options.facts;
4929
+ const file = options.file;
4930
+ if (bases.length === 0 || facts === void 0 || file === void 0 || !pattern.clientBuilders.includes(method)) {
4931
+ return false;
4932
+ }
4933
+ const classKey = enclosingClassKey(node, file);
4934
+ return classKey !== null && reachesBase(facts, classKey, bases);
4935
+ }
4936
+ function argumentAt(args, place) {
4937
+ const keyword = place.keyword;
4938
+ return (keyword === void 0 ? void 0 : args.keyword[keyword]) ?? args.positional[place.at];
4939
+ }
4940
+ function stringAt(args, place, facts) {
4941
+ const written = argumentAt(args, place);
4942
+ if (written === void 0) {
4943
+ return null;
4944
+ }
4945
+ const pieces = piecesOf2(evaluatedValue(written, facts));
4946
+ const only = pieces.length === 1 ? pieces[0] : void 0;
4947
+ if (only === void 0 || only.kind !== "text" || only.options.length !== 1) {
4948
+ return null;
4949
+ }
4950
+ return only.options[0] ?? null;
4951
+ }
4952
+ function statementAt(args, place, facts, bindPlaceholder) {
4953
+ const written = argumentAt(args, place);
4954
+ if (written === void 0) {
4955
+ return null;
4956
+ }
4957
+ const parts = literalParts(statementValue(evaluatedValue(written, facts)));
4958
+ if (parts === null) {
4959
+ return null;
4960
+ }
4961
+ const statement = sqlFromParts(bindsSplit(parts, bindPlaceholder));
4962
+ return statement.trim() === "" ? null : statement;
4963
+ }
4964
+ function bindsSplit(parts, bindPlaceholder) {
4965
+ if (bindPlaceholder === void 0 || bindPlaceholder === "") {
4966
+ return [...parts];
4967
+ }
4968
+ return parts.flatMap((part) => part.split(bindPlaceholder));
4969
+ }
4970
+ function statementValue(value) {
4971
+ if (value.kind !== "sequence") {
4972
+ return value;
4973
+ }
4974
+ const first = value.items[0];
4975
+ return first === void 0 ? value : first.value;
4976
+ }
4977
+ function literalParts(value) {
4978
+ if (value.kind !== "string") {
4979
+ return null;
4980
+ }
4981
+ const parts = [""];
4982
+ for (const piece of value.pieces) {
4983
+ const only = piece.kind === "text" ? piece.options : [];
4984
+ if (only.length === 1) {
4985
+ parts[parts.length - 1] += only[0] ?? "";
4986
+ continue;
4987
+ }
4988
+ parts.push("");
4989
+ }
4990
+ return parts;
4991
+ }
4992
+
4993
+ // src/storage.ts
4994
+ function children3(node) {
4995
+ return node.namedChildren.filter((child) => child !== null);
4996
+ }
4997
+ function receiverOf2(node) {
4998
+ return node.type === "call" ? field(node, "receiver") : null;
4999
+ }
5000
+ function methodOf2(node) {
5001
+ return field(node, "method")?.text ?? "";
5002
+ }
4325
5003
  function rootConstant(node) {
4326
- const receiver = receiverOf(node);
5004
+ const receiver = receiverOf2(node);
4327
5005
  if (receiver === null) {
4328
5006
  return null;
4329
5007
  }
@@ -4338,17 +5016,6 @@ function isConstant(node) {
4338
5016
  function constantName(constant2) {
4339
5017
  return constant2.type === "constant" ? constant2.text : compoundName(constant2);
4340
5018
  }
4341
- function reachesBase(facts, classKey, bases) {
4342
- askResolution2(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4343
- return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4344
- }
4345
- function classBehind(facts, file, constant2) {
4346
- const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4347
- const bound = new Set(
4348
- facts.lookup("binds", 0, key).map((row) => String(row[1]))
4349
- );
4350
- return bound.size === 1 ? [...bound][0] : void 0;
4351
- }
4352
5019
  function modelPattern(constant2, file, options) {
4353
5020
  const classKey = classBehind(options.facts, file, constant2);
4354
5021
  if (classKey === void 0) {
@@ -4363,7 +5030,7 @@ function chainLinks(call) {
4363
5030
  let current = call;
4364
5031
  while (current !== null && current.type === "call") {
4365
5032
  links.push(current);
4366
- current = receiverOf(current);
5033
+ current = receiverOf2(current);
4367
5034
  }
4368
5035
  return links;
4369
5036
  }
@@ -4376,9 +5043,17 @@ function kindOfCall(pattern, method) {
4376
5043
  }
4377
5044
  return void 0;
4378
5045
  }
5046
+ function libraryCallOf(pattern, method) {
5047
+ const place = pattern.statements?.[method];
5048
+ if (place !== void 0) {
5049
+ return { how: "statement", place };
5050
+ }
5051
+ const kind = kindOfCall(pattern, method);
5052
+ return kind === void 0 ? void 0 : { how: "rows", kind };
5053
+ }
4379
5054
  function someLibraryDefines(options, method) {
4380
5055
  return options.patterns.some(
4381
- (pattern) => kindOfCall(pattern, method) !== void 0
5056
+ (pattern) => libraryCallOf(pattern, method) !== void 0
4382
5057
  );
4383
5058
  }
4384
5059
  function argumentsOf2(call) {
@@ -4413,7 +5088,7 @@ function hasPositionalArgument(call, facts) {
4413
5088
  );
4414
5089
  }
4415
5090
  function columnsAskedFor(call, pattern, facts) {
4416
- if (!(pattern.columnArguments ?? []).includes(methodOf(call))) {
5091
+ if (!(pattern.columnArguments ?? []).includes(methodOf2(call))) {
4417
5092
  return [];
4418
5093
  }
4419
5094
  return argumentsOf2(call).map((argument) => stringValueOf(argument, facts)).filter((column) => column !== null);
@@ -4423,14 +5098,14 @@ function primaryKeySelector(call, pattern, facts) {
4423
5098
  if (byPrimaryKey === void 0) {
4424
5099
  return [];
4425
5100
  }
4426
- const picks = byPrimaryKey.methods.includes(methodOf(call)) && hasPositionalArgument(call, facts);
5101
+ const picks = byPrimaryKey.methods.includes(methodOf2(call)) && hasPositionalArgument(call, facts);
4427
5102
  return picks ? [byPrimaryKey.column] : [];
4428
5103
  }
4429
5104
  function selectorOf(call, pattern, facts) {
4430
5105
  const picked = [];
4431
5106
  for (const link of chainLinks(call)) {
4432
5107
  picked.push(...primaryKeySelector(link, pattern, facts));
4433
- if (pattern.reads.includes(methodOf(link))) {
5108
+ if (pattern.reads.includes(methodOf2(link))) {
4434
5109
  picked.push(...keywordKeys(link, facts));
4435
5110
  }
4436
5111
  }
@@ -4447,10 +5122,10 @@ function fieldsOf(call, pattern, kind, facts) {
4447
5122
  ];
4448
5123
  }
4449
5124
  function storageEffect(call, container, pattern, kind, selector, fields) {
4450
- const operation = methodOf(call);
5125
+ const operation = methodOf2(call);
4451
5126
  return {
4452
5127
  type: "interaction",
4453
- binding: storageBinding({
5128
+ binding: storageBinding2({
4454
5129
  recognition: "ruby-storage",
4455
5130
  storageSystem: pattern.storageSystem,
4456
5131
  scope: "default",
@@ -4467,7 +5142,7 @@ function storageEffect(call, container, pattern, kind, selector, fields) {
4467
5142
  };
4468
5143
  }
4469
5144
  function classSettledOn(facts, key) {
4470
- askResolution2(facts, [key], "wanted", RUBY_PROGRAM);
5145
+ askResolution3(facts, [key], "wanted", RUBY_PROGRAM);
4471
5146
  const objects = new Set(
4472
5147
  facts.lookup("wantedObjectOf", 0, key).map((row) => String(row[1]))
4473
5148
  );
@@ -4499,7 +5174,7 @@ function receiverClass(call, file, options, enclosing) {
4499
5174
  if (constant2 !== null) {
4500
5175
  return constantReceiver(constant2, file, options.facts);
4501
5176
  }
4502
- const receiver = receiverOf(call);
5177
+ const receiver = receiverOf2(call);
4503
5178
  if (receiver === null) {
4504
5179
  return void 0;
4505
5180
  }
@@ -4507,60 +5182,91 @@ function receiverClass(call, file, options, enclosing) {
4507
5182
  }
4508
5183
  function libraryCallIn(call, options) {
4509
5184
  return chainLinks(call).find(
4510
- (link) => someLibraryDefines(options, methodOf(link))
5185
+ (link) => someLibraryDefines(options, methodOf2(link))
4511
5186
  ) ?? null;
4512
5187
  }
5188
+ function modelStatementEffects(call, place, pattern, options) {
5189
+ const args = readCallArgs(field(call, "arguments"));
5190
+ return statementEffects(
5191
+ call,
5192
+ statementAt(args, place, options.facts, pattern.bindPlaceholder),
5193
+ NOWHERE,
5194
+ { storageSystem: pattern.storageSystem, dialect: pattern.storageSystem }
5195
+ );
5196
+ }
5197
+ function callbacksRunBy(facts, classKey, pattern, writeMethod) {
5198
+ const events = pattern.callbacks?.eventOf[writeMethod] ?? [];
5199
+ if (events.length === 0) {
5200
+ return [];
5201
+ }
5202
+ askResolution3(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
5203
+ const found = /* @__PURE__ */ new Map();
5204
+ for (const row of facts.lookup("wantedCallbackMethod", 0, classKey)) {
5205
+ if (events.includes(String(row[1]))) {
5206
+ const key = String(row[3]);
5207
+ found.set(key, { name: String(row[2]), key });
5208
+ }
5209
+ }
5210
+ return [...found.values()];
5211
+ }
5212
+ function callbackEffect(callback) {
5213
+ return {
5214
+ type: "invocation",
5215
+ callee: callback.name,
5216
+ args: [],
5217
+ async: false
5218
+ };
5219
+ }
4513
5220
  function modelCallEffects(call, file, options, enclosing) {
4514
5221
  const worked = libraryCallIn(call, options);
4515
5222
  if (worked === null) {
4516
5223
  return [];
4517
5224
  }
4518
- const method = methodOf(worked);
5225
+ const method = methodOf2(worked);
4519
5226
  const target = receiverClass(worked, file, options, enclosing);
4520
5227
  if (target === void 0) {
4521
5228
  return [];
4522
5229
  }
4523
5230
  for (const pattern of options.patterns) {
4524
- const kind = kindOfCall(pattern, method);
4525
- if (kind === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
5231
+ const library = libraryCallOf(pattern, method);
5232
+ if (library === void 0 || !reachesBase(options.facts, target.classKey, pattern.baseClasses)) {
4526
5233
  continue;
4527
5234
  }
4528
5235
  if (projectDeclares(options.facts, target.classKey, method)) {
4529
5236
  return [];
4530
5237
  }
5238
+ if (library.how === "statement") {
5239
+ return modelStatementEffects(worked, library.place, pattern, options);
5240
+ }
5241
+ const ran = library.kind === "write" ? callbacksRunBy(options.facts, target.classKey, pattern, method) : [];
4531
5242
  return [
4532
5243
  storageEffect(
4533
5244
  worked,
4534
5245
  target.container,
4535
5246
  pattern,
4536
- kind,
5247
+ library.kind,
4537
5248
  selectorOf(worked, pattern, options.facts),
4538
- fieldsOf(worked, pattern, kind, options.facts)
4539
- )
5249
+ fieldsOf(worked, pattern, library.kind, options.facts)
5250
+ ),
5251
+ ...ran.map(callbackEffect)
4540
5252
  ];
4541
5253
  }
4542
5254
  return [];
4543
5255
  }
4544
- function isLoader(node, loader) {
4545
- if (node === null) {
4546
- return false;
4547
- }
4548
- if (node.type === "identifier") {
4549
- return node.text === loader.loader;
4550
- }
4551
- return node.type === "call" && receiverOf(node) === null && methodOf(node) === loader.loader;
4552
- }
4553
- function loaderPick(call, loader) {
4554
- const method = methodOf(call);
4555
- const receiver = receiverOf(call);
4556
- if (receiver === null) {
4557
- return loader.shortcuts.includes(method) ? call : null;
5256
+ function callbacksReached(call, file, options, enclosing = null) {
5257
+ const worked = libraryCallIn(call, options);
5258
+ const target = worked === null ? void 0 : receiverClass(worked, file, options, enclosing);
5259
+ if (worked === null || target === void 0) {
5260
+ return [];
4558
5261
  }
4559
- if (!loader.reads.includes(method) || receiver.type !== "call") {
4560
- return null;
5262
+ const method = methodOf2(worked);
5263
+ for (const pattern of options.patterns) {
5264
+ if (libraryCallOf(pattern, method)?.how !== "rows" || kindOfCall(pattern, method) !== "write" || !reachesBase(options.facts, target.classKey, pattern.baseClasses) || projectDeclares(options.facts, target.classKey, method)) {
5265
+ continue;
5266
+ }
5267
+ return callbacksRunBy(options.facts, target.classKey, pattern, method);
4561
5268
  }
4562
- const picks = methodOf(receiver) === loader.pick && isLoader(receiverOf(receiver), loader);
4563
- return picks ? receiver : null;
5269
+ return [];
4564
5270
  }
4565
5271
  function loaderCallEffects(call, file, options) {
4566
5272
  const effects = [];
@@ -4586,10 +5292,24 @@ function loaderCallEffects(call, file, options) {
4586
5292
  }
4587
5293
  function effectsOfCall(call, file, options, enclosing) {
4588
5294
  const onModel = modelCallEffects(call, file, options, enclosing);
4589
- return onModel.length > 0 ? onModel : loaderCallEffects(call, file, options);
5295
+ if (onModel.length > 0) {
5296
+ return onModel;
5297
+ }
5298
+ const throughLoader = loaderCallEffects(call, file, options);
5299
+ if (throughLoader.length > 0) {
5300
+ return throughLoader;
5301
+ }
5302
+ return rawSqlEffects(call, {
5303
+ facts: options.facts,
5304
+ patterns: options.rawSql ?? [],
5305
+ file
5306
+ });
4590
5307
  }
4591
5308
  function storageClaims(call, file, options, enclosing = null) {
4592
- return options.patterns.length > 0 && effectsOfCall(call, file, options, enclosing).length > 0;
5309
+ return saysAnything(options) && effectsOfCall(call, file, options, enclosing).length > 0;
5310
+ }
5311
+ function saysAnything(options) {
5312
+ return options.patterns.length > 0 || (options.rawSql ?? []).length > 0;
4593
5313
  }
4594
5314
  function receiverKeysToAsk(chains, file, options, enclosing) {
4595
5315
  const keys = [];
@@ -4598,7 +5318,7 @@ function receiverKeysToAsk(chains, file, options, enclosing) {
4598
5318
  if (worked === null || rootConstant(worked) !== null) {
4599
5319
  continue;
4600
5320
  }
4601
- const receiver = receiverOf(worked);
5321
+ const receiver = receiverOf2(worked);
4602
5322
  if (receiver !== null) {
4603
5323
  keys.push(readKey(file, receiver, enclosing));
4604
5324
  }
@@ -4606,18 +5326,18 @@ function receiverKeysToAsk(chains, file, options, enclosing) {
4606
5326
  return keys;
4607
5327
  }
4608
5328
  function storageEffects(calls, file, options, enclosing = null) {
4609
- if (options.patterns.length === 0) {
5329
+ if (!saysAnything(options)) {
4610
5330
  return [];
4611
5331
  }
4612
5332
  const partOfOne = /* @__PURE__ */ new Set();
4613
5333
  for (const call of calls) {
4614
- const receiver = receiverOf(call);
5334
+ const receiver = receiverOf2(call);
4615
5335
  if (receiver !== null) {
4616
5336
  partOfOne.add(receiver.id);
4617
5337
  }
4618
5338
  }
4619
5339
  const chains = calls.filter((call) => !partOfOne.has(call.id));
4620
- askResolution2(
5340
+ askResolution3(
4621
5341
  options.facts,
4622
5342
  receiverKeysToAsk(chains, file, options, enclosing),
4623
5343
  "wanted",
@@ -4742,14 +5462,18 @@ async function discoverUnits(root, options) {
4742
5462
  file: options.absoluteFile ?? options.filePath
4743
5463
  }));
4744
5464
  const units = [];
5465
+ const clientOptions = {
5466
+ filePath: options.filePath,
5467
+ ...options.facts === void 0 ? {} : { facts: options.facts }
5468
+ };
5469
+ askClientCallReads(
5470
+ root,
5471
+ options.packs.flatMap((pack) => pack.clients ?? []),
5472
+ clientOptions
5473
+ );
4745
5474
  for (const pack of options.packs) {
4746
5475
  for (const pattern of pack.clients ?? []) {
4747
- units.push(
4748
- ...clientCallUnits(root, pack, pattern, {
4749
- filePath: options.filePath,
4750
- ...options.facts === void 0 ? {} : { facts: options.facts }
4751
- })
4752
- );
5476
+ units.push(...clientCallUnits(root, pack, pattern, clientOptions));
4753
5477
  }
4754
5478
  }
4755
5479
  for (const info of classes) {
@@ -5082,8 +5806,12 @@ function bodyOfMethod(method, file, bodyRead = {}) {
5082
5806
  bodyRead.facts
5083
5807
  );
5084
5808
  const storage = bodyRead.storage;
5809
+ const facts = bodyRead.facts;
5085
5810
  const extra = [
5086
- ...envReadEffects(method),
5811
+ ...envReadEffects(
5812
+ method,
5813
+ facts === void 0 ? void 0 : { db: facts, file }
5814
+ ),
5087
5815
  ...storage === void 0 ? [] : storageEffects(callsUnder2(method), file, storage, method)
5088
5816
  ];
5089
5817
  return {
@@ -5859,6 +6587,47 @@ function buildRubyExtractionReport(args) {
5859
6587
  });
5860
6588
  }
5861
6589
 
6590
+ // src/facts/callbacks.ts
6591
+ function callbacksIn(patterns) {
6592
+ return patterns.flatMap(
6593
+ (pattern) => pattern.callbacks === void 0 ? [] : [pattern.callbacks]
6594
+ );
6595
+ }
6596
+ function emitClassCallbacks(db, file, classNode, declared) {
6597
+ const body = field(classNode, "body");
6598
+ if (body === null || declared.length === 0) {
6599
+ return;
6600
+ }
6601
+ const classKey = nodeId(file, classNode);
6602
+ for (const statement of runStatements(body)) {
6603
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
6604
+ continue;
6605
+ }
6606
+ const called = field(statement, "method")?.text ?? "";
6607
+ const args = readCallArgs(field(statement, "arguments"));
6608
+ for (const callbacks of declared) {
6609
+ const events = eventsOf(args, called, callbacks, db);
6610
+ for (const event of events) {
6611
+ for (const method of symbolArgumentNames(args, db)) {
6612
+ db.add("classCallback", [classKey, event, method]);
6613
+ }
6614
+ }
6615
+ }
6616
+ }
6617
+ }
6618
+ function eventsOf(args, called, callbacks, db) {
6619
+ const covered = callbacks.registeredBy[called];
6620
+ if (covered === void 0) {
6621
+ return [];
6622
+ }
6623
+ const written = args.keyword[callbacks.eventKeyword];
6624
+ const narrowed = written === void 0 ? null : stringValueOf(written, db);
6625
+ if (narrowed === null) {
6626
+ return covered;
6627
+ }
6628
+ return covered.filter((event) => event === narrowed);
6629
+ }
6630
+
5862
6631
  // src/facts.ts
5863
6632
  import path3 from "path";
5864
6633
  import { SKIP_CHILDREN as SKIP_CHILDREN3, walkDescendants as walkDescendants3 } from "@suss/extractor";
@@ -5975,6 +6744,10 @@ function calleeSpellings(calls, ctx) {
5975
6744
  };
5976
6745
  }
5977
6746
  function resolveCallee(call, site, ctx, read) {
6747
+ const throughLoader = loaderSourceCallee(call, site, ctx);
6748
+ if (throughLoader !== null) {
6749
+ return throughLoader;
6750
+ }
5978
6751
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor(call, site);
5979
6752
  if (spelling.kind === "stopped") {
5980
6753
  return spelling;
@@ -5984,6 +6757,21 @@ function resolveCallee(call, site, ctx, read) {
5984
6757
  }
5985
6758
  return asCallee(spelling, outcomeFor(spelling.key, ctx, read), site, ctx);
5986
6759
  }
6760
+ function loaderSourceCallee(call, site, ctx) {
6761
+ for (const loader of ctx.loaders) {
6762
+ const constant2 = pickedSource(call, loader);
6763
+ const source = loader.source;
6764
+ if (constant2 === null || source === void 0) {
6765
+ continue;
6766
+ }
6767
+ const classKey = classBehind(ctx.facts, site.file, constant2);
6768
+ const qualifiedName = classKey === void 0 ? void 0 : ctx.classNames.get(classKey);
6769
+ if (qualifiedName !== void 0) {
6770
+ return methodOnAncestryOf(qualifiedName, source.method, ctx);
6771
+ }
6772
+ }
6773
+ return null;
6774
+ }
5987
6775
  function outcomeFor(key, ctx, read) {
5988
6776
  return read?.outcomes.get(key) ?? calleeOutcomeOf(ctx.facts, key, RUBY_PROGRAM);
5989
6777
  }
@@ -6165,6 +6953,7 @@ async function reachedFunctions(seeds, options) {
6165
6953
  const parameterCallsByKey = /* @__PURE__ */ new Map();
6166
6954
  const propertyReadsByKey = /* @__PURE__ */ new Map();
6167
6955
  const passedPositions = /* @__PURE__ */ new Set();
6956
+ const followedKeys = /* @__PURE__ */ new Set();
6168
6957
  for (const seed of seeds) {
6169
6958
  seedKeys.add(seed.key);
6170
6959
  functionByKey.set(seed.key, {
@@ -6212,6 +7001,9 @@ async function reachedFunctions(seeds, options) {
6212
7001
  for (const position of scan.passedPositions) {
6213
7002
  passedPositions.add(position);
6214
7003
  }
7004
+ if (scan.followed.length > 0) {
7005
+ followedKeys.add(key);
7006
+ }
6215
7007
  for (const target of scan.followed) {
6216
7008
  const calleeKey = keyOf(target);
6217
7009
  if (!functionByKey.has(calleeKey)) {
@@ -6248,7 +7040,8 @@ async function reachedFunctions(seeds, options) {
6248
7040
  argTargetsByKey,
6249
7041
  parameterCallsByKey,
6250
7042
  propertyReadsByKey,
6251
- passedPositions
7043
+ passedPositions,
7044
+ followedKeys
6252
7045
  };
6253
7046
  }
6254
7047
  function keyOf(target) {
@@ -6287,13 +7080,9 @@ function methodReferenceSymbol(node) {
6287
7080
  return argChildren.length === 1 && argChildren[0]?.type === "simple_symbol" ? argChildren[0] : null;
6288
7081
  }
6289
7082
  function bodyOf2(source, options) {
6290
- const site = {
6291
- file: source.file,
6292
- method: source.node,
6293
- owner: keyOf(source),
6294
- enclosingQualifiedName: source.enclosingQualifiedName
6295
- };
6296
- const written = field(source.node, "body") === null ? [] : bodyCalls(source.node, options.inheritedMethods);
7083
+ const site = siteOf2(source);
7084
+ const read = readableBodyOf(source.node);
7085
+ const written = read === null ? [] : bodyCalls(read, options.inheritedMethods);
6297
7086
  const calls = callsReported(
6298
7087
  written,
6299
7088
  (call) => mightReadAsACall(call, site, options.context)
@@ -6305,6 +7094,17 @@ function bodyOf2(source, options) {
6305
7094
  written: calls.map((call) => ({ call, site }))
6306
7095
  };
6307
7096
  }
7097
+ function readableBodyOf(node) {
7098
+ return node.type === PROGRAM_TYPE ? moduleScopeBody(node) : methodBody(node);
7099
+ }
7100
+ function siteOf2(source) {
7101
+ return {
7102
+ file: source.file,
7103
+ method: source.node.type === PROGRAM_TYPE ? null : source.node,
7104
+ owner: keyOf(source),
7105
+ enclosingQualifiedName: source.enclosingQualifiedName
7106
+ };
7107
+ }
6308
7108
  function callsMade(asked, site, ctx, spellings) {
6309
7109
  return asked.filter(
6310
7110
  (call) => !isArglessReceiverCall(call) || readsAsACall(call, site, ctx, spellings)
@@ -6312,7 +7112,7 @@ function callsMade(asked, site, ctx, spellings) {
6312
7112
  }
6313
7113
  function scanBody(source, ctx, options, read) {
6314
7114
  const displayPathOf = options.displayPathOf;
6315
- if (field(source.node, "body") === null) {
7115
+ if (readableBodyOf(source.node) === null) {
6316
7116
  return EMPTY_SCAN;
6317
7117
  }
6318
7118
  const site = read.site;
@@ -6355,9 +7155,34 @@ function scanBody(source, ctx, options, read) {
6355
7155
  }
6356
7156
  }
6357
7157
  };
7158
+ const followCallback = (name, key) => {
7159
+ const target = ctx.definitions.get(key);
7160
+ if (target === void 0) {
7161
+ return;
7162
+ }
7163
+ const targetKey = keyOf(target);
7164
+ if (!seen.has(targetKey)) {
7165
+ seen.add(targetKey);
7166
+ followed2.push(target);
7167
+ }
7168
+ placements.place(name, {
7169
+ file: displayPathOf(target.file),
7170
+ span: spanOf(target.node)
7171
+ });
7172
+ };
6358
7173
  for (const call of calls) {
6359
7174
  const callee = calleeText(call);
6360
7175
  const outcome = resolveCallee(call, site, ctx, read.spellings);
7176
+ if (options.storage !== void 0) {
7177
+ for (const callback of callbacksReached(
7178
+ call,
7179
+ source.file,
7180
+ options.storage,
7181
+ site.method
7182
+ )) {
7183
+ followCallback(callback.name, callback.key);
7184
+ }
7185
+ }
6361
7186
  const placed = outcome.kind === "followed" ? {
6362
7187
  file: displayPathOf(outcome.target.file),
6363
7188
  span: spanOf(outcome.target.node)
@@ -6489,8 +7314,8 @@ function parameterName(param) {
6489
7314
  }
6490
7315
 
6491
7316
  // src/reach/context.ts
6492
- var METHOD_TYPES4 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
6493
- async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map()) {
7317
+ var METHOD_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
7318
+ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dynamicNames = /* @__PURE__ */ new Map(), loaders = []) {
6494
7319
  const blocksByQualifiedName = /* @__PURE__ */ new Map();
6495
7320
  const classes = [];
6496
7321
  const classNames = /* @__PURE__ */ new Map();
@@ -6500,7 +7325,7 @@ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dyna
6500
7325
  classes.push({ file, info });
6501
7326
  classNames.set(nodeId(file, info.node), info.qualifiedName);
6502
7327
  for (const statement of namedChildren(info.bodyNode)) {
6503
- if (METHOD_TYPES4.has(statement.type)) {
7328
+ if (METHOD_TYPES3.has(statement.type)) {
6504
7329
  classOfMethod.set(nodeId(file, statement), info.qualifiedName);
6505
7330
  }
6506
7331
  }
@@ -6566,7 +7391,8 @@ async function buildReachContext(files, facts, bodyBlocks = NO_BODY_BLOCKS, dyna
6566
7391
  classNames,
6567
7392
  definitions,
6568
7393
  bodyBlocks,
6569
- dynamicNames
7394
+ dynamicNames,
7395
+ loaders
6570
7396
  };
6571
7397
  }
6572
7398
  function namedChildren(node) {
@@ -6634,6 +7460,7 @@ function factsForFile(options) {
6634
7460
  const db = new Database2();
6635
7461
  const bodyBlocks = bodyBlocksIn(options.packs);
6636
7462
  emitValueFacts(db, options.file, options.root, bodyBlocks);
7463
+ emitEnvFacts(db, options.file, options.root);
6637
7464
  emitConstantBindings(db, [
6638
7465
  collectFileConstants(
6639
7466
  options.file,
@@ -6650,6 +7477,19 @@ function factsForFile(options) {
6650
7477
  addPackWords(db, packWordsOf(options.packs));
6651
7478
  return db;
6652
7479
  }
7480
+ function moduleInitUnit(options) {
7481
+ const raw = moduleInitStructure({
7482
+ name: options.name,
7483
+ file: options.file,
7484
+ range: options.range,
7485
+ effects: options.effects
7486
+ });
7487
+ const branch = raw.branches[0];
7488
+ if (branch !== void 0) {
7489
+ branch.effects = options.calls;
7490
+ }
7491
+ return raw;
7492
+ }
6653
7493
  function alreadyDiscovered(seen, raw) {
6654
7494
  const reported = raw.identity.exportPath?.join(".") ?? raw.identity.name;
6655
7495
  const boundary = raw.boundaryBinding === null ? "" : boundaryKey(raw.boundaryBinding) ?? "";
@@ -6705,6 +7545,7 @@ async function extractRubyProject(options) {
6705
7545
  }
6706
7546
  parsed.push({ file, root });
6707
7547
  emitValueFacts(db, file, root, bodyBlocks);
7548
+ emitEnvFacts(db, file, root);
6708
7549
  for (const [key, method] of methodDefinitionsIn(file, root)) {
6709
7550
  definitions.set(key, method);
6710
7551
  }
@@ -6721,6 +7562,15 @@ async function extractRubyProject(options) {
6721
7562
  }
6722
7563
  bindEvaluator(db, { files: parsed, definitions });
6723
7564
  addPackWords(db, packWordsOf(options.packs));
7565
+ const callbacks = callbacksIn(
7566
+ options.packs.flatMap((pack) => pack.storage ?? [])
7567
+ );
7568
+ for (const { file, root } of parsed) {
7569
+ walkDefinitions(
7570
+ root,
7571
+ (info) => emitClassCallbacks(db, file, info.node, callbacks)
7572
+ );
7573
+ }
6724
7574
  return readDynamicNames(
6725
7575
  db,
6726
7576
  new Map(parsed.map(({ file, root }) => [file, root]))
@@ -6728,11 +7578,17 @@ async function extractRubyProject(options) {
6728
7578
  });
6729
7579
  const storagePatterns = options.packs.flatMap((pack) => pack.storage ?? []);
6730
7580
  const loaderPatterns = options.packs.flatMap((pack) => pack.loaders ?? []);
6731
- const storage = storagePatterns.length > 0 ? { facts: db, patterns: storagePatterns, loaders: loaderPatterns } : void 0;
7581
+ const rawSqlPatterns = options.packs.flatMap((pack) => pack.rawSql ?? []);
7582
+ const storage = storagePatterns.length > 0 || rawSqlPatterns.length > 0 ? {
7583
+ facts: db,
7584
+ patterns: storagePatterns,
7585
+ loaders: loaderPatterns,
7586
+ rawSql: rawSqlPatterns
7587
+ } : void 0;
6732
7588
  const inheritedMethods = inheritedMethodsIn(options.packs);
6733
7589
  const reachContext = await timer.timeAsync(
6734
7590
  "discover",
6735
- () => buildReachContext(parsed, db, bodyBlocks, dynamicNames)
7591
+ () => buildReachContext(parsed, db, bodyBlocks, dynamicNames, loaderPatterns)
6736
7592
  );
6737
7593
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path4.relative(options.workspaceRoot, file) : file;
6738
7594
  const seeds = [];
@@ -6782,23 +7638,34 @@ async function extractRubyProject(options) {
6782
7638
  });
6783
7639
  }
6784
7640
  }
6785
- const loadTimeReads = timer.time("discover", () => envReadEffects(root));
6786
- if (loadTimeReads.length > 0) {
6787
- const summary = timer.time(
6788
- "summarize",
6789
- () => assembleSummary2(
6790
- moduleInitStructure({
6791
- name: path4.basename(displayPath),
6792
- file: displayPath,
6793
- range: rangeOf(root),
6794
- effects: loadTimeReads
6795
- }),
6796
- { gapHandling: "permissive" }
6797
- )
6798
- );
6799
- summary.confidence = { source: "inferred_static", level: "low" };
6800
- found.push({ summary });
6801
- }
7641
+ const moduleKey = nodeId(file, root);
7642
+ seedKeys.add(moduleKey);
7643
+ seeds.push({
7644
+ key: moduleKey,
7645
+ file,
7646
+ node: root,
7647
+ enclosingQualifiedName: null
7648
+ });
7649
+ const loadTime = timer.time("discover", () => ({
7650
+ reads: envReadEffects(root, { db, file }),
7651
+ calls: moduleScopeInvocationEffects(
7652
+ root,
7653
+ inheritedMethods,
7654
+ EVERY_ARGLESS_CALL,
7655
+ db
7656
+ )
7657
+ }));
7658
+ found.push({
7659
+ raw: moduleInitUnit({
7660
+ name: path4.basename(displayPath),
7661
+ file: displayPath,
7662
+ range: rangeOf(root),
7663
+ effects: loadTime.reads,
7664
+ calls: loadTime.calls
7665
+ }),
7666
+ seedKey: moduleKey,
7667
+ onlyIfItReaches: loadTime.reads.length === 0
7668
+ });
6802
7669
  }
6803
7670
  for (const pack of options.packs) {
6804
7671
  for (const pattern of pack.discovery) {
@@ -6836,6 +7703,9 @@ async function extractRubyProject(options) {
6836
7703
  continue;
6837
7704
  }
6838
7705
  const { raw, seedKey } = entry;
7706
+ if (entry.onlyIfItReaches === true && (seedKey === null || !reached.followedKeys.has(seedKey))) {
7707
+ continue;
7708
+ }
6839
7709
  if (seedKey !== null) {
6840
7710
  dropPropertyReads(raw, reached.propertyReadsByKey.get(seedKey));
6841
7711
  }
@@ -6945,20 +7815,9 @@ function findRubyFiles(root) {
6945
7815
 
6946
7816
  // src/facts/why.ts
6947
7817
  var WITNESS_RULES = [...RESOLUTION_RULES, ...RUBY_RULES];
6948
- var METHOD_TYPES5 = /* @__PURE__ */ new Set(["method", "singleton_method"]);
6949
7818
  function namedChildrenOf(node) {
6950
7819
  return node.namedChildren.filter((child) => child !== null);
6951
7820
  }
6952
- function enclosingMethod2(node) {
6953
- let current = node.parent;
6954
- while (current !== null) {
6955
- if (METHOD_TYPES5.has(current.type)) {
6956
- return current;
6957
- }
6958
- current = current.parent;
6959
- }
6960
- return null;
6961
- }
6962
7821
  function indexFile(file, root, locations) {
6963
7822
  const walk = (node) => {
6964
7823
  locations.set(nodeId(file, node), { file, node });
@@ -6994,7 +7853,7 @@ function width(node) {
6994
7853
  return node.endIndex - node.startIndex;
6995
7854
  }
6996
7855
  function askedKey(value) {
6997
- const enclosing = enclosingMethod2(value.node);
7856
+ const enclosing = enclosingDefinition(value.node);
6998
7857
  return calleeKeyOf(value.file, value.node, enclosing) ?? readKey(value.file, value.node, enclosing);
6999
7858
  }
7000
7859
  var RubyWhySession = class {
@@ -7209,6 +8068,8 @@ export {
7209
8068
  resolveConstantFile,
7210
8069
  rubyStubEvidence,
7211
8070
  runStatements,
8071
+ storageClaims,
8072
+ storageEffects,
7212
8073
  stringLiteralValue,
7213
8074
  stringValueOf,
7214
8075
  symbolValue,