@suss/adapter-ruby 0.35.0 → 0.36.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
@@ -627,6 +627,8 @@ import {
627
627
  readUses,
628
628
  resolutionProgram,
629
629
  resolutionUnderProgram,
630
+ SELF_AS_CLASS,
631
+ SELF_AS_INSTANCE,
630
632
  writtenValueOf as sharedWrittenValueOf,
631
633
  writtenValueUnder as sharedWrittenValueUnder,
632
634
  USES_QUESTION
@@ -720,6 +722,8 @@ var INVOKE_METHOD = "call";
720
722
  var BLOCK_WRAPPERS = /* @__PURE__ */ new Set(["proc", "lambda"]);
721
723
  var PROC_CLASS = "Proc";
722
724
  var CONSTRUCTOR = "new";
725
+ var INITIALIZE_METHOD = "initialize";
726
+ var CLASS_OF_INSTANCE = "class";
723
727
 
724
728
  // src/facts/resolve.ts
725
729
  import { resolvedFunctions, settledFunction } from "@suss/resolution";
@@ -747,6 +751,26 @@ var RUBY_RULES = alsoSteps([
747
751
  lit("readsProperty", v("c"), v("o"), constant(CONSTRUCTOR)),
748
752
  lit("call", v("r"), v("c"))
749
753
  ]
754
+ ),
755
+ // `new` in a class method makes one of the class the method runs on,
756
+ // and `self.class` in an instance method is that class.
757
+ rule(
758
+ "selfBehind",
759
+ [v("r"), v("s"), SELF_AS_INSTANCE],
760
+ [
761
+ lit("call", v("r"), v("c")),
762
+ lit("readsProperty", v("c"), v("o"), constant(CONSTRUCTOR)),
763
+ lit("selfBehind", v("o"), v("s"), SELF_AS_CLASS)
764
+ ]
765
+ ),
766
+ rule(
767
+ "selfBehind",
768
+ [v("r"), v("s"), SELF_AS_CLASS],
769
+ [
770
+ lit("call", v("r"), v("c")),
771
+ lit("readsProperty", v("c"), v("o"), constant(CLASS_OF_INSTANCE)),
772
+ lit("selfBehind", v("o"), v("s"), SELF_AS_INSTANCE)
773
+ ]
750
774
  )
751
775
  ]);
752
776
  var RUBY_PROGRAM = resolutionProgram(RUBY_RULES);
@@ -796,6 +820,8 @@ import {
796
820
  classMemberName,
797
821
  NAMED_STORE_NAME,
798
822
  RECEIVER_STORE_NAME,
823
+ SELF_AS_CLASS_NAME,
824
+ SELF_AS_INSTANCE_NAME,
799
825
  SPREAD_ENTRY,
800
826
  startsAtName,
801
827
  UNPLACED_STORE_NAME,
@@ -2488,9 +2514,8 @@ function emitCandidates(emitter, key, group) {
2488
2514
  add(emitter, "writesAllStated", key);
2489
2515
  }
2490
2516
  }
2491
- var INITIALIZE_METHOD = "initialize";
2492
- function receiverKeyOf(emitter, method, classKey) {
2493
- return definesClassMethod(method, emitter.bodyBlocks) ? classKey : `${nodeId(emitter.filePath, method)}#self`;
2517
+ function receiverKeyOf(file, method) {
2518
+ return `${nodeId(file, method)}#self`;
2494
2519
  }
2495
2520
  var CONSTANT_REF_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
2496
2521
  function mixedInConstants(body, callName, statements) {
@@ -2510,11 +2535,20 @@ function emitMixinFacts(emitter, classKey, body) {
2510
2535
  }
2511
2536
  function emitMethodOfClass(within, classKey, method, offeredOnClass) {
2512
2537
  const stored = /* @__PURE__ */ new Map();
2513
- const receiverKey = receiverKeyOf(within, method, classKey);
2514
- const onInstance = receiverKey !== classKey;
2538
+ const receiverKey = receiverKeyOf(within.filePath, method);
2539
+ const onInstance = !definesClassMethod(method, within.bodyBlocks);
2515
2540
  if (onInstance) {
2516
2541
  add(within, "instanceOf", receiverKey, classKey);
2542
+ } else {
2543
+ add(within, "binds", receiverKey, classKey);
2517
2544
  }
2545
+ add(
2546
+ within,
2547
+ "selfReceiver",
2548
+ nodeId(within.filePath, method),
2549
+ receiverKey,
2550
+ onInstance ? SELF_AS_INSTANCE_NAME : SELF_AS_CLASS_NAME
2551
+ );
2518
2552
  const funcKey = emitMethodFacts(
2519
2553
  {
2520
2554
  ...within,
@@ -4216,7 +4250,10 @@ async function definedInFileOf(fileConstant, qualifiedName, lookup) {
4216
4250
  if (filePath === null) {
4217
4251
  return null;
4218
4252
  }
4219
- const fileRoot = await lookup.parsedFile(filePath);
4253
+ return blocksDefinedIn(filePath, qualifiedName, lookup.parsedFile);
4254
+ }
4255
+ async function blocksDefinedIn(filePath, qualifiedName, parsedFile) {
4256
+ const fileRoot = await parsedFile(filePath);
4220
4257
  if (fileRoot === null) {
4221
4258
  return null;
4222
4259
  }
@@ -4296,9 +4333,16 @@ function unreadConstant(candidates) {
4296
4333
  const bare = candidates.at(-1);
4297
4334
  return bare === void 0 ? [] : [{ type: "unfollowed", name: bare }];
4298
4335
  }
4299
- function definitionOf(qualifiedName, lookup) {
4336
+ async function definitionOf(qualifiedName, lookup) {
4300
4337
  const local = lookup.localDefinition?.(qualifiedName);
4301
- return local !== void 0 && local !== null ? Promise.resolve(local) : reachDefinition(qualifiedName, lookup);
4338
+ if (local !== void 0 && local !== null) {
4339
+ return local;
4340
+ }
4341
+ const inRun = await lookup.definedInRun?.(qualifiedName);
4342
+ if (inRun !== void 0 && inRun !== null) {
4343
+ return inRun;
4344
+ }
4345
+ return reachDefinition(qualifiedName, lookup);
4302
4346
  }
4303
4347
  async function mixinChain(self, callName, lookup, active, present) {
4304
4348
  let chain = [];
@@ -4443,7 +4487,69 @@ import {
4443
4487
  graphqlResolverBinding,
4444
4488
  restBinding as restBinding2
4445
4489
  } from "@suss/behavioral-ir";
4446
- import { absentReading as absentReading3, unreadableReading as unreadableReading2 } from "@suss/extractor";
4490
+ import { absentReading as absentReading3, unreadableReading as unreadableReading3 } from "@suss/extractor";
4491
+
4492
+ // src/baseClass.ts
4493
+ import { answersFor, askResolution as askResolution2, spanOfKey as spanOfKey2 } from "@suss/resolution";
4494
+ function classBehind(facts, file, constant2) {
4495
+ const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
4496
+ const bound = new Set(
4497
+ facts.lookup("binds", 0, key).map((row) => String(row[1]))
4498
+ );
4499
+ return bound.size === 1 ? [...bound][0] : void 0;
4500
+ }
4501
+ function classSettledOn(facts, key) {
4502
+ askResolution2(facts, [key], "wanted", RUBY_PROGRAM);
4503
+ const objects = answersFor(facts, "wantedObjectOf", key);
4504
+ return objects.length === 1 ? objects[0] : void 0;
4505
+ }
4506
+ function declaredName(facts, classKey) {
4507
+ const names = new Set(
4508
+ facts.lookup("rbConstantName", 0, classKey).map((row) => String(row[1]))
4509
+ );
4510
+ return names.size === 1 ? [...names][0] : void 0;
4511
+ }
4512
+ function classesDeclaredInRun(facts, parsedFile) {
4513
+ const known = /* @__PURE__ */ new Map();
4514
+ const read = async (qualifiedName) => {
4515
+ const files = /* @__PURE__ */ new Set();
4516
+ for (const row of facts.lookup("rbConstantName", 1, qualifiedName)) {
4517
+ const span = spanOfKey2(String(row[0]));
4518
+ if (span !== null) {
4519
+ files.add(span.file);
4520
+ }
4521
+ }
4522
+ const blocks = [];
4523
+ for (const file of files) {
4524
+ blocks.push(
4525
+ ...await blocksDefinedIn(file, qualifiedName, parsedFile) ?? []
4526
+ );
4527
+ }
4528
+ return blocks.length === 0 ? null : blocks;
4529
+ };
4530
+ return (qualifiedName) => {
4531
+ let found = known.get(qualifiedName);
4532
+ if (found === void 0) {
4533
+ found = read(qualifiedName);
4534
+ known.set(qualifiedName, found);
4535
+ }
4536
+ return found;
4537
+ };
4538
+ }
4539
+ function reachesBase(facts, classKey, bases) {
4540
+ askResolution2(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
4541
+ return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
4542
+ }
4543
+ function enclosingClassKey(node, file) {
4544
+ let current = node.parent;
4545
+ while (current !== null) {
4546
+ if (NESTING_TYPES.has(current.type)) {
4547
+ return nodeId(file, current);
4548
+ }
4549
+ current = current.parent;
4550
+ }
4551
+ return null;
4552
+ }
4447
4553
 
4448
4554
  // src/clientCalls.ts
4449
4555
  import {
@@ -4456,7 +4562,7 @@ import {
4456
4562
  import { hostOf, pathOf } from "@suss/values";
4457
4563
 
4458
4564
  // src/paths/effects.ts
4459
- import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
4565
+ import { sharedGatingConditions, statementSites } from "@suss/extractor";
4460
4566
  import { constantOf, literalOf as literalOf4 } from "@suss/values";
4461
4567
 
4462
4568
  // src/paths/lowering.ts
@@ -4668,6 +4774,7 @@ var Lowerer = class {
4668
4774
  );
4669
4775
  return {
4670
4776
  kind: "try",
4777
+ lines: rangeOf(node),
4671
4778
  tryBody: tryBody.map((statement) => this.lower(statement)),
4672
4779
  catchBody: rescues2.length > 0 ? rescues2.flatMap((clause) => this.lowerBlock(field(clause, "body"))) : null,
4673
4780
  finallyBody: ensureClause === null ? null : this.lowerBlock(ensureClause),
@@ -4759,7 +4866,8 @@ var Lowerer = class {
4759
4866
  kind: "loop",
4760
4867
  condition: handleOf(header),
4761
4868
  body: this.lowerBlock(field(node, "body")),
4762
- exitKind: exitKindOf(node)
4869
+ exitKind: exitKindOf(node),
4870
+ lines: rangeOf(node)
4763
4871
  },
4764
4872
  [header]
4765
4873
  );
@@ -4775,7 +4883,8 @@ var Lowerer = class {
4775
4883
  kind: "loop",
4776
4884
  condition: callHeaderOf(node, attachedBlock),
4777
4885
  body: this.lowerBlock(field(attachedBlock, "body") ?? attachedBlock),
4778
- exitKind: exitKindOf(node)
4886
+ exitKind: exitKindOf(node),
4887
+ lines: rangeOf(node)
4779
4888
  },
4780
4889
  [node]
4781
4890
  );
@@ -4819,6 +4928,9 @@ var BLOCK_TYPES5 = /* @__PURE__ */ new Set([
4819
4928
  "block"
4820
4929
  ]);
4821
4930
  function literalArgOf(node, facts) {
4931
+ if (node.type === "nil") {
4932
+ return { kind: "null" };
4933
+ }
4822
4934
  const value = evaluatedValue(node, facts);
4823
4935
  const text3 = literalOf4(value);
4824
4936
  if (text3 !== null) {
@@ -4956,13 +5068,21 @@ function argsOf(call, facts) {
4956
5068
  function enclosingStatement(call, body) {
4957
5069
  let current = call;
4958
5070
  while (current.parent !== null) {
4959
- if (BLOCK_TYPES5.has(current.parent.type) || current.parent.id === body.id) {
5071
+ if (listsStatements(current.parent, body)) {
4960
5072
  return current;
4961
5073
  }
4962
5074
  current = current.parent;
4963
5075
  }
4964
5076
  return current;
4965
5077
  }
5078
+ function listsStatements(node, body) {
5079
+ if (BLOCK_TYPES5.has(node.type) || node.id === body.id) {
5080
+ return true;
5081
+ }
5082
+ const begin = node.type === "ensure" ? node.parent : node;
5083
+ const around = begin?.type === "begin" ? begin.parent : null;
5084
+ return around !== null && around !== void 0 && (BLOCK_TYPES5.has(around.type) || around.id === body.id);
5085
+ }
4966
5086
  function invocationEffects(definitionNode, inherited = NO_INHERITED_METHODS, keepsArglessCall = NO_ARGLESS_CALLS, facts) {
4967
5087
  return invocationSites(
4968
5088
  definitionNode,
@@ -4987,36 +5107,13 @@ function moduleScopeInvocationEffects(root, inherited = NO_INHERITED_METHODS, ke
4987
5107
  ).map((site) => site.effect);
4988
5108
  }
4989
5109
  function effectsOfBody(read, inherited, keepsArglessCall, facts) {
4990
- const body = read.body;
4991
5110
  const calls = callsReported(bodyCalls(read, inherited), keepsArglessCall);
4992
5111
  if (calls.length === 0) {
4993
5112
  return [];
4994
5113
  }
4995
- const statementOf2 = /* @__PURE__ */ new Map();
4996
- const byStatement = new NodeMap();
4997
- const statements = [];
4998
- for (const call of calls) {
4999
- const written = enclosingStatement(call, body);
5000
- const statement = byStatement.get(written);
5001
- if (statement === void 0) {
5002
- byStatement.set(written, written);
5003
- statements.push(written);
5004
- }
5005
- statementOf2.set(call.id, statement ?? written);
5006
- }
5007
- const lowered = lowerRubyBody(body, statements);
5008
- const enumerated = enumerateOrDegrade(
5009
- {
5010
- statements: lowered.statements,
5011
- terminalsByStmt: lowered.terminalsByStmt
5012
- },
5013
- statements
5014
- );
5114
+ const sites = sitesIn(read.body, calls);
5015
5115
  return calls.map((call) => {
5016
- const statement = statementOf2.get(call.id);
5017
- const conditions = sharedGatingConditions(
5018
- statement === void 0 ? void 0 : enumerated.byTerminal.get(statement)
5019
- );
5116
+ const conditions = sharedGatingConditions(sites.pathsTo(call));
5020
5117
  return {
5021
5118
  call,
5022
5119
  effect: {
@@ -5024,11 +5121,26 @@ function effectsOfBody(read, inherited, keepsArglessCall, facts) {
5024
5121
  callee: calleeText(call),
5025
5122
  args: argsOf(call, facts),
5026
5123
  async: false,
5027
- ...conditions.length > 0 ? { preconditions: conditions } : {}
5028
- }
5124
+ ...conditions.length > 0 ? { preconditions: conditions } : {},
5125
+ location: rangeOf(call)
5126
+ },
5127
+ site: sites.siteOf(call, conditions)
5029
5128
  };
5030
5129
  });
5031
5130
  }
5131
+ function sitesIn(body, nodes) {
5132
+ return statementSites(nodes, {
5133
+ statementOf: (node) => enclosingStatement(node, body),
5134
+ lower: (statements) => lowerRubyBody(body, statements),
5135
+ firstLine: (node) => rangeOf(node).start
5136
+ });
5137
+ }
5138
+ function effectSitesIn(read, nodes) {
5139
+ if (read === null || nodes.length === 0) {
5140
+ return (node) => ({ line: rangeOf(node).start });
5141
+ }
5142
+ return sitesIn(read.body, nodes).plainSiteOf;
5143
+ }
5032
5144
 
5033
5145
  // src/paths/predicates.ts
5034
5146
  var OPERATORS = {
@@ -5415,15 +5527,10 @@ function raiseTerminal(call) {
5415
5527
  }
5416
5528
  function throughResponseHelpers(branches, sites, options) {
5417
5529
  const handOffs = [];
5418
- for (const { call, effect } of sites) {
5530
+ for (const { call, site } of sites) {
5419
5531
  const helper = helperBranchesAt(call, options);
5420
5532
  if (helper !== null) {
5421
- handOffs.push({
5422
- helper,
5423
- preconditions: effect.preconditions,
5424
- line: rangeOf(call).start,
5425
- at: rangeOf(call)
5426
- });
5533
+ handOffs.push({ ...site, helper, at: rangeOf(call) });
5427
5534
  }
5428
5535
  }
5429
5536
  return handOffs.length === 0 ? [...branches] : branches.flatMap((branch) => splitAtHandOffs(branch, handOffs));
@@ -5486,8 +5593,8 @@ function sameNode(a, b) {
5486
5593
  // src/responseStatus.ts
5487
5594
  import {
5488
5595
  absentReading as absentReading2,
5489
- enumerateOrDegrade as enumerateOrDegrade2,
5490
- guardsHoldOn,
5596
+ effectsFiringOn,
5597
+ enumerateOrDegrade,
5491
5598
  unreadableReading,
5492
5599
  writtenReading as writtenReading2
5493
5600
  } from "@suss/extractor";
@@ -5499,31 +5606,7 @@ import { constantOf as constantOf2, literalOf as literalOf6 } from "@suss/values
5499
5606
 
5500
5607
  // src/storage.ts
5501
5608
  import { storageBinding as storageBinding2 } from "@suss/ir-core";
5502
- import { answersFor, askResolution as askResolution3, classMemberName as classMemberName2 } from "@suss/resolution";
5503
-
5504
- // src/baseClass.ts
5505
- import { askResolution as askResolution2 } from "@suss/resolution";
5506
- function classBehind(facts, file, constant2) {
5507
- const key = constant2.type === "constant" ? `${file}#${constant2.text}` : nodeId(file, constant2);
5508
- const bound = new Set(
5509
- facts.lookup("binds", 0, key).map((row) => String(row[1]))
5510
- );
5511
- return bound.size === 1 ? [...bound][0] : void 0;
5512
- }
5513
- function reachesBase(facts, classKey, bases) {
5514
- askResolution2(facts, [classKey], "wantedAncestry", RUBY_PROGRAM);
5515
- return facts.lookup("wantedBaseName", 0, classKey).some((row) => bases.includes(String(row[1])));
5516
- }
5517
- function enclosingClassKey(node, file) {
5518
- let current = node.parent;
5519
- while (current !== null) {
5520
- if (NESTING_TYPES.has(current.type)) {
5521
- return nodeId(file, current);
5522
- }
5523
- current = current.parent;
5524
- }
5525
- return null;
5526
- }
5609
+ import { askResolution as askResolution3, classMemberName as classMemberName2 } from "@suss/resolution";
5527
5610
 
5528
5611
  // src/loaders.ts
5529
5612
  function receiverOf(node) {
@@ -5668,7 +5751,8 @@ function effectOf(call, store, address, access) {
5668
5751
  fields: access.fields,
5669
5752
  operation,
5670
5753
  ...access.selector.length > 0 ? { selector: access.selector } : {}
5671
- }
5754
+ },
5755
+ location: rangeOf(call)
5672
5756
  };
5673
5757
  }
5674
5758
  function addressBehind(receiver, pattern, options, hops) {
@@ -5979,20 +6063,10 @@ function storageEffect(call, container, pattern, kind, selector, fields) {
5979
6063
  fields,
5980
6064
  operation,
5981
6065
  ...selector.length > 0 ? { selector } : {}
5982
- }
6066
+ },
6067
+ location: rangeOf(call)
5983
6068
  };
5984
6069
  }
5985
- function classSettledOn(facts, key) {
5986
- askResolution3(facts, [key], "wanted", RUBY_PROGRAM);
5987
- const objects = answersFor(facts, "wantedObjectOf", key);
5988
- return objects.length === 1 ? objects[0] : void 0;
5989
- }
5990
- function declaredName(facts, classKey) {
5991
- const names = new Set(
5992
- facts.lookup("rbConstantName", 0, classKey).map((row) => String(row[1]))
5993
- );
5994
- return names.size === 1 ? [...names][0] : void 0;
5995
- }
5996
6070
  function projectDeclares(facts, classKey, method) {
5997
6071
  const spellings = /* @__PURE__ */ new Set([method, classMemberName2(method)]);
5998
6072
  return facts.lookup("wantedDeclaredName", 0, classKey).some((row) => spellings.has(String(row[1])));
@@ -6049,12 +6123,13 @@ function callbacksRunBy(facts, classKey, pattern, writeMethod) {
6049
6123
  }
6050
6124
  return [...found.values()];
6051
6125
  }
6052
- function callbackEffect(callback) {
6126
+ function callbackEffect(callback, write) {
6053
6127
  return {
6054
6128
  type: "invocation",
6055
6129
  callee: callback.name,
6056
6130
  args: [],
6057
- async: false
6131
+ async: false,
6132
+ location: rangeOf(write)
6058
6133
  };
6059
6134
  }
6060
6135
  function modelCallEffects(call, file, options, enclosing) {
@@ -6093,7 +6168,7 @@ function modelCallEffects(call, file, options, enclosing) {
6093
6168
  options.statesSlots(access, slots);
6094
6169
  }
6095
6170
  }
6096
- return [access, ...ran.map(callbackEffect)];
6171
+ return [access, ...ran.map((callback) => callbackEffect(callback, worked))];
6097
6172
  }
6098
6173
  return [];
6099
6174
  }
@@ -6231,11 +6306,12 @@ function storageEffects(calls, file, options, enclosing = null) {
6231
6306
  "wanted",
6232
6307
  RUBY_PROGRAM
6233
6308
  );
6234
- const effects = [];
6235
- for (const call of chains) {
6236
- effects.push(...effectsOfCall(call, file, options, enclosing));
6237
- }
6238
- return effects;
6309
+ return chains.flatMap(
6310
+ (call) => effectsOfCall(call, file, options, enclosing).map((effect) => ({
6311
+ effect,
6312
+ at: call
6313
+ }))
6314
+ );
6239
6315
  }
6240
6316
 
6241
6317
  // src/provenance.ts
@@ -6624,13 +6700,13 @@ function readingOfCall(call, declaration, names, options) {
6624
6700
  }
6625
6701
  return writtenReading2(status, rangeOf(argument));
6626
6702
  }
6627
- function effectsReaching(effects, conditions) {
6628
- return effects.filter(
6629
- (effect) => guardsHoldOn(
6630
- effect.type === "invocation" ? effect.preconditions : void 0,
6631
- conditions
6632
- )
6633
- );
6703
+ function withEffectsThatRun(branch, body) {
6704
+ const extra = effectsFiringOn(body.extraEffects ?? [], branch);
6705
+ return {
6706
+ ...branch,
6707
+ effects: effectsFiringOn(body.effects, branch),
6708
+ ...extra.length === 0 ? {} : { extraEffects: extra }
6709
+ };
6634
6710
  }
6635
6711
  function conditionOf(condition, inputs) {
6636
6712
  return {
@@ -6690,57 +6766,66 @@ function helperResponse(helper, pattern, facts, boundStatuses = /* @__PURE__ */
6690
6766
  rangeOf(helper)
6691
6767
  );
6692
6768
  }
6693
- function branchOf(outcome, pattern, effects, extraEffects, options) {
6769
+ function branchOf(outcome, pattern, body, options) {
6694
6770
  if (outcome.fellThrough === true && options.fallthrough === "handOn") {
6695
- return handsOnBranch(outcome, effects, extraEffects);
6771
+ return handsOnBranch(outcome, body);
6696
6772
  }
6697
- return {
6698
- conditions: outcome.conditions,
6699
- terminal: {
6700
- kind: "response",
6701
- statusCode: null,
6702
- body: null,
6703
- exceptionType: null,
6704
- message: null,
6705
- component: null,
6706
- renderTree: null,
6707
- delegateTarget: null,
6708
- emitEvent: null,
6709
- location: outcome.location
6710
- },
6711
- statusCodeReading: {
6712
- reading: outcome.reading,
6713
- libraryDefault: pattern.defaultStatusCode
6773
+ return withEffectsThatRun(
6774
+ {
6775
+ conditions: outcome.conditions,
6776
+ terminal: {
6777
+ kind: "response",
6778
+ statusCode: null,
6779
+ body: null,
6780
+ exceptionType: null,
6781
+ message: null,
6782
+ component: null,
6783
+ renderTree: null,
6784
+ delegateTarget: null,
6785
+ emitEvent: null,
6786
+ location: outcome.location
6787
+ },
6788
+ statusCodeReading: {
6789
+ reading: outcome.reading,
6790
+ libraryDefault: pattern.defaultStatusCode
6791
+ },
6792
+ ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
6793
+ effects: [],
6794
+ location: outcome.location,
6795
+ ...loopAroundOf(outcome),
6796
+ isDefault: outcome.conditions.length === 0
6714
6797
  },
6715
- ...outcome.body === void 0 ? {} : { bodyShapeReading: { reading: outcome.body } },
6716
- effects: effectsReaching(effects, outcome.conditions),
6717
- ...extraEffects === void 0 ? {} : { extraEffects },
6718
- location: outcome.location,
6719
- isDefault: outcome.conditions.length === 0
6720
- };
6798
+ body
6799
+ );
6721
6800
  }
6722
- function handsOnBranch(outcome, effects, extraEffects) {
6723
- return {
6724
- conditions: outcome.conditions,
6725
- terminal: {
6726
- kind: "delegate",
6727
- statusCode: null,
6728
- body: null,
6729
- exceptionType: null,
6730
- message: null,
6731
- component: null,
6732
- renderTree: null,
6733
- delegateTarget: null,
6734
- emitEvent: null,
6735
- location: outcome.location
6801
+ function handsOnBranch(outcome, body) {
6802
+ return withEffectsThatRun(
6803
+ {
6804
+ conditions: outcome.conditions,
6805
+ terminal: {
6806
+ kind: "delegate",
6807
+ statusCode: null,
6808
+ body: null,
6809
+ exceptionType: null,
6810
+ message: null,
6811
+ component: null,
6812
+ renderTree: null,
6813
+ delegateTarget: null,
6814
+ emitEvent: null,
6815
+ location: outcome.location
6816
+ },
6817
+ effects: [],
6818
+ location: outcome.location,
6819
+ ...loopAroundOf(outcome),
6820
+ isDefault: outcome.conditions.length === 0
6736
6821
  },
6737
- effects: effectsReaching(effects, outcome.conditions),
6738
- ...extraEffects === void 0 ? {} : { extraEffects },
6739
- location: outcome.location,
6740
- isDefault: outcome.conditions.length === 0
6741
- };
6822
+ body
6823
+ );
6824
+ }
6825
+ function loopAroundOf(placed) {
6826
+ return placed.loopAround === void 0 ? {} : { loopAround: placed.loopAround };
6742
6827
  }
6743
- function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = []) {
6828
+ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raises2 = [], endingOfReturn = (_statement, location) => returnTerminal(location)) {
6744
6829
  const body = field(method, "body");
6745
6830
  if (body === null) {
6746
6831
  return null;
@@ -6753,7 +6838,7 @@ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raise
6753
6838
  ...raises2.map((raise) => raise.call)
6754
6839
  ];
6755
6840
  const lowered = lowerRubyBody(body, terminals, exitCalls);
6756
- const enumerated = enumerateOrDegrade2(
6841
+ const enumerated = enumerateOrDegrade(
6757
6842
  {
6758
6843
  statements: lowered.statements,
6759
6844
  terminalsByStmt: lowered.terminalsByStmt,
@@ -6767,30 +6852,42 @@ function returnPathBranches(method, effects, exits = [], facts, keepsArms, raise
6767
6852
  facts
6768
6853
  );
6769
6854
  const branches = [];
6770
- const push = (paths, terminal) => {
6855
+ const push = (paths, terminal, at) => {
6856
+ const placement = at === null ? void 0 : enumerated.placement.get(at);
6771
6857
  for (const path7 of paths) {
6772
6858
  const conditions = path7.map(
6773
6859
  (condition) => conditionOf(condition, inputs)
6774
6860
  );
6775
- branches.push({
6776
- conditions,
6777
- terminal,
6778
- effects: effectsReaching(effects, conditions),
6779
- location: terminal.location,
6780
- isDefault: conditions.length === 0
6781
- });
6861
+ branches.push(
6862
+ withEffectsThatRun(
6863
+ {
6864
+ conditions,
6865
+ terminal,
6866
+ effects: [],
6867
+ location: terminal.location,
6868
+ ...loopAroundOf({ loopAround: placement?.loopAround }),
6869
+ isDefault: conditions.length === 0
6870
+ },
6871
+ { effects }
6872
+ )
6873
+ );
6782
6874
  }
6783
6875
  };
6784
6876
  for (const statement of returns) {
6785
6877
  push(
6786
6878
  enumerated.byTerminal.get(statement) ?? [],
6787
- returnTerminal(rangeOf(statement))
6879
+ endingOfReturn(statement, rangeOf(statement)),
6880
+ statement
6788
6881
  );
6789
6882
  }
6790
6883
  for (const ending of [...exits, ...raises2]) {
6791
- push(enumerated.byTerminal.get(ending.call) ?? [], ending.terminal);
6884
+ push(
6885
+ enumerated.byTerminal.get(ending.call) ?? [],
6886
+ ending.terminal,
6887
+ ending.call
6888
+ );
6792
6889
  }
6793
- push(enumerated.fallthrough, returnTerminal(rangeOf(method)));
6890
+ push(enumerated.fallthrough, endingOfReturn(null, rangeOf(method)), null);
6794
6891
  return branches.length === 0 ? null : branches;
6795
6892
  }
6796
6893
  function returnTerminal(location) {
@@ -6831,7 +6928,7 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6831
6928
  (node) => lowered.terminalHome.has(node) && collectResponseCalls(node, responders, []).length === 0
6832
6929
  );
6833
6930
  const terminals = [...responses, ...bareReturns];
6834
- const enumerated = enumerateOrDegrade2(
6931
+ const enumerated = enumerateOrDegrade(
6835
6932
  {
6836
6933
  statements: lowered.statements,
6837
6934
  terminalsByStmt: lowered.terminalsByStmt
@@ -6850,12 +6947,14 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6850
6947
  const responder = responderOf(terminal, responders);
6851
6948
  const reading = responder === void 0 ? absentReading2 : readingOfResponder(terminal, responder, statusNames, options);
6852
6949
  const body2 = responder?.kind === "declared" ? bodyReadingOfCall(terminal, responder.declaration, options.facts) : void 0;
6950
+ const loopAround = enumerated.placement.get(terminal)?.loopAround;
6853
6951
  for (const path7 of enumerated.byTerminal.get(terminal) ?? []) {
6854
6952
  outcomes.push({
6855
6953
  conditions: conditionsOf(path7),
6856
6954
  reading,
6857
6955
  ...body2 === void 0 ? {} : { body: body2 },
6858
6956
  location: rangeOf(terminal),
6957
+ loopAround,
6859
6958
  // A bare `return` wrote no response, so a filter that takes it
6860
6959
  // hands the request on the same as one that reaches its end.
6861
6960
  ...responder === void 0 ? { fellThrough: true } : {}
@@ -6871,6 +6970,7 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6871
6970
  });
6872
6971
  }
6873
6972
  const read = options.raises;
6973
+ const placed = { effects, extraEffects };
6874
6974
  const raised = raises2.flatMap((call) => {
6875
6975
  const terminal = raisedTerminal(
6876
6976
  pattern,
@@ -6878,25 +6978,26 @@ function responseBranches(method, pattern, effects, extraEffects, options = {})
6878
6978
  read?.handlers ?? [],
6879
6979
  rangeOf(call)
6880
6980
  );
6881
- return (enumerated.byTerminal.get(call) ?? []).map((path7) => {
6882
- const conditions = conditionsOf(path7);
6883
- return {
6884
- conditions,
6885
- terminal,
6886
- effects: effectsReaching(effects, conditions),
6887
- ...extraEffects === void 0 ? {} : { extraEffects },
6888
- location: terminal.location,
6889
- isDefault: false
6890
- };
6891
- });
6981
+ const loopAround = enumerated.placement.get(call)?.loopAround;
6982
+ return (enumerated.byTerminal.get(call) ?? []).map(
6983
+ (path7) => withEffectsThatRun(
6984
+ {
6985
+ conditions: conditionsOf(path7),
6986
+ terminal,
6987
+ effects: [],
6988
+ location: terminal.location,
6989
+ ...loopAroundOf({ loopAround }),
6990
+ isDefault: false
6991
+ },
6992
+ placed
6993
+ )
6994
+ );
6892
6995
  });
6893
6996
  if (outcomes.length === 0 && raised.length === 0) {
6894
6997
  return null;
6895
6998
  }
6896
6999
  return [
6897
- ...outcomes.map(
6898
- (outcome) => branchOf(outcome, pattern, effects, extraEffects, options)
6899
- ),
7000
+ ...outcomes.map((outcome) => branchOf(outcome, pattern, placed, options)),
6900
7001
  ...raised
6901
7002
  ];
6902
7003
  }
@@ -7164,7 +7265,7 @@ function callerBranches(method, reading) {
7164
7265
  const keepsArms = (condition) => testsStatus(predicateOf(condition), reading.statusMembers);
7165
7266
  const found = returnPathBranches(
7166
7267
  method,
7167
- effects,
7268
+ sites,
7168
7269
  [],
7169
7270
  reading.facts,
7170
7271
  keepsArms,
@@ -7623,7 +7724,7 @@ function envReadEffects(root, facts) {
7623
7724
  at: (node) => {
7624
7725
  const read = envReadAt(node);
7625
7726
  if (read !== null) {
7626
- slots.push({ read });
7727
+ slots.push({ read, at: node });
7627
7728
  return;
7628
7729
  }
7629
7730
  if (helpers !== null && node.type === "call") {
@@ -7636,9 +7737,12 @@ function envReadEffects(root, facts) {
7636
7737
  const throughHelpers = helpers === null ? new NodeMap() : helperReads(calls, helpers);
7637
7738
  return slots.flatMap((slot) => {
7638
7739
  if ("read" in slot) {
7639
- return [configReadEffect(slot.read)];
7740
+ return [{ effect: configReadEffect(slot.read, slot.at), at: slot.at }];
7640
7741
  }
7641
- return (throughHelpers.get(slot.call) ?? []).map(configReadEffect);
7742
+ return (throughHelpers.get(slot.call) ?? []).map((read) => ({
7743
+ effect: configReadEffect(read, slot.call),
7744
+ at: slot.call
7745
+ }));
7642
7746
  });
7643
7747
  }
7644
7748
  function helperFactsOf(facts) {
@@ -7794,16 +7898,17 @@ function readsAtCall(db, call, args) {
7794
7898
  }
7795
7899
  return [...byName].map(([name, defaulted]) => ({ name, defaulted }));
7796
7900
  }
7797
- function configReadEffect(read) {
7901
+ function configReadEffect(read, at) {
7798
7902
  return {
7799
7903
  type: "interaction",
7800
7904
  binding: runtimeConfigBinding({ recognition: RUBY_ENV_RECOGNITION }),
7801
- callee: `ENV["${read.name}"]`,
7905
+ callee: read.name === null ? at.text : `ENV["${read.name}"]`,
7802
7906
  interaction: {
7803
7907
  class: "config-read",
7804
7908
  name: read.name,
7805
7909
  defaulted: read.defaulted
7806
- }
7910
+ },
7911
+ location: rangeOf(at)
7807
7912
  };
7808
7913
  }
7809
7914
  function envReadAt(node) {
@@ -7811,8 +7916,7 @@ function envReadAt(node) {
7811
7916
  if (site === null) {
7812
7917
  return null;
7813
7918
  }
7814
- const name = stringLiteralValue(site.name);
7815
- return name === null ? null : { name, defaulted: site.defaulted };
7919
+ return { name: stringLiteralValue(site.name), defaulted: site.defaulted };
7816
7920
  }
7817
7921
  function envSiteAt(node) {
7818
7922
  const spelling = envSpellingAt(node);
@@ -8008,25 +8112,27 @@ function filterUnit(filter, pattern, displayPath, body, reading = {}) {
8008
8112
  };
8009
8113
  }
8010
8114
  function handsOn(range, body) {
8011
- return {
8012
- conditions: [],
8013
- terminal: {
8014
- kind: "delegate",
8015
- statusCode: null,
8016
- body: null,
8017
- exceptionType: null,
8018
- message: null,
8019
- component: null,
8020
- renderTree: null,
8021
- delegateTarget: null,
8022
- emitEvent: null,
8023
- location: range
8115
+ return withEffectsThatRun(
8116
+ {
8117
+ conditions: [],
8118
+ terminal: {
8119
+ kind: "delegate",
8120
+ statusCode: null,
8121
+ body: null,
8122
+ exceptionType: null,
8123
+ message: null,
8124
+ component: null,
8125
+ renderTree: null,
8126
+ delegateTarget: null,
8127
+ emitEvent: null,
8128
+ location: range
8129
+ },
8130
+ effects: [],
8131
+ location: range,
8132
+ isDefault: true
8024
8133
  },
8025
- effects: body.effects ?? [],
8026
- ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
8027
- location: range,
8028
- isDefault: true
8029
- };
8134
+ { effects: body.effects ?? [], extraEffects: body.extraEffects }
8135
+ );
8030
8136
  }
8031
8137
  function calledName(statement) {
8032
8138
  if (statement.type !== "call" || field(statement, "receiver") !== null) {
@@ -8163,34 +8269,214 @@ function actionsUnder(node, facts) {
8163
8269
  return found === null ? null : new Set(found);
8164
8270
  }
8165
8271
 
8166
- // src/streamWrites.ts
8167
- import { ioBinding } from "@suss/behavioral-ir";
8168
- import { SKIP_CHILDREN as SKIP_CHILDREN3, walkDescendants as walkDescendants3 } from "@suss/extractor";
8169
- var RUBY_STREAM_RECOGNITION = "ruby-stdlib";
8170
- var KERNEL_WRITES = /* @__PURE__ */ new Map([
8171
- ["puts", "stdout"],
8172
- ["print", "stdout"],
8173
- ["warn", "stderr"],
8174
- ["abort", "stderr"]
8175
- ]);
8176
- var STREAM_RECEIVERS = /* @__PURE__ */ new Map([
8177
- ["$stdout", "stdout"],
8178
- ["STDOUT", "stdout"],
8179
- ["$stderr", "stderr"],
8180
- ["STDERR", "stderr"]
8181
- ]);
8182
- var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
8183
- var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
8184
- function streamWriteEffects(root) {
8185
- const found = [];
8186
- walkDescendants3(root, null, children, {
8187
- at: (node) => {
8188
- const write = node.type === "call" ? streamWriteAt(node) : null;
8189
- if (write !== null) {
8190
- found.push(write);
8191
- }
8192
- },
8193
- into: (node) => DEFERRED_BODY_TYPES2.has(node.type) ? SKIP_CHILDREN3 : null
8272
+ // src/resolverHooks.ts
8273
+ import {
8274
+ SKIP_CHILDREN as SKIP_CHILDREN3,
8275
+ unreadableReading as unreadableReading2,
8276
+ walkDescendants as walkDescendants3
8277
+ } from "@suss/extractor";
8278
+ import { constantOf as constantOf4 } from "@suss/values";
8279
+ function hooksRun(ancestry, hooks, read) {
8280
+ const definitions = [];
8281
+ const unsettled = [];
8282
+ for (const name of hooks.methods) {
8283
+ const found = definitionsRun(ancestry, name, read);
8284
+ definitions.push(...found.definitions);
8285
+ if (found.unsettled !== null) {
8286
+ unsettled.push({ name, reason: found.unsettled });
8287
+ }
8288
+ }
8289
+ return { definitions, unsettled };
8290
+ }
8291
+ function definitionsRun(ancestry, name, read) {
8292
+ const definitions = [];
8293
+ let rest = ancestry;
8294
+ while (rest.length > 0) {
8295
+ const lookup = methodInAncestry(rest, name, read);
8296
+ if (lookup.type !== "found") {
8297
+ return { definitions, unsettled: unsettledReason(lookup) };
8298
+ }
8299
+ definitions.push({ name, method: lookup.method, block: lookup.block });
8300
+ if (!callsSuper(lookup.method)) {
8301
+ break;
8302
+ }
8303
+ const at = rest.findIndex(
8304
+ (entry) => entry.type === "bodies" && entry.blocks.includes(lookup.block)
8305
+ );
8306
+ rest = rest.slice(Math.max(at, 0) + 1);
8307
+ }
8308
+ return { definitions, unsettled: null };
8309
+ }
8310
+ function unsettledReason(lookup) {
8311
+ if (lookup.type !== "unsettled") {
8312
+ return null;
8313
+ }
8314
+ return lookup.cause === "dynamicDefine" || lookup.definedLater === true ? lookup.reason : null;
8315
+ }
8316
+ function callsSuper(method) {
8317
+ let found = false;
8318
+ walkDescendants3(method, null, children, {
8319
+ at: (node) => {
8320
+ found ||= isSuper(node);
8321
+ },
8322
+ into: (node) => node !== method && METHOD_TYPES.has(node.type) ? SKIP_CHILDREN3 : null
8323
+ });
8324
+ return found;
8325
+ }
8326
+ function isSuper(node) {
8327
+ return node.type === "super" || node.type === "call" && field(node, "method")?.type === "super";
8328
+ }
8329
+ function hookUnit(hook, input) {
8330
+ const { body, facts } = input;
8331
+ const range = rangeOf(hook.method);
8332
+ const effects = body.effects ?? [];
8333
+ const branches = (returnPathBranches(
8334
+ hook.method,
8335
+ effects,
8336
+ [],
8337
+ facts,
8338
+ void 0,
8339
+ raiseEndings(hook.method),
8340
+ (statement, location) => hookEnding(hook.method, statement, location, input)
8341
+ ) ?? [emptyBodyBranch(range)]).map(
8342
+ (branch) => withEffectsThatRun(branch, {
8343
+ effects,
8344
+ extraEffects: body.extraEffects
8345
+ })
8346
+ );
8347
+ return {
8348
+ identity: {
8349
+ name: hook.name,
8350
+ nameKind: "binding",
8351
+ kind: "middleware",
8352
+ file: input.displayPath,
8353
+ range,
8354
+ span: spanOf(hook.method),
8355
+ exportName: hook.name,
8356
+ exportPath: [hook.block.info.qualifiedName, hook.name]
8357
+ },
8358
+ boundaryBinding: null,
8359
+ parameters: [],
8360
+ branches: withSlotSources(branches, body.provenance),
8361
+ bodyContent: body.bodyContent ?? "absent",
8362
+ dependencyCalls: [],
8363
+ declaredContract: null
8364
+ };
8365
+ }
8366
+ function hookReference(hook, displayPath) {
8367
+ return {
8368
+ file: displayPath,
8369
+ name: hook.name,
8370
+ line: rangeOf(hook.method).start
8371
+ };
8372
+ }
8373
+ function unsettledHookReading(className, unsettled, range) {
8374
+ return unreadableReading2(
8375
+ `This field's ${className} could be stopped by a ${unsettled.name} hook ${unsettled.reason}, so whether one runs was not settled here`,
8376
+ range
8377
+ );
8378
+ }
8379
+ function hookEnding(method, statement, location, input) {
8380
+ const values = statement === null ? lastStatement(method) : returnedValues(statement);
8381
+ return stopsTheField(values, input) ? endingTerminal("return", location) : goOn(location);
8382
+ }
8383
+ function lastStatement(method) {
8384
+ const body = field(method, "body");
8385
+ const last = body === null ? void 0 : bodyStatements(body).at(-1);
8386
+ return last === void 0 ? [] : [last];
8387
+ }
8388
+ function returnedValues(statement) {
8389
+ const list = children(statement)[0];
8390
+ if (list === void 0) {
8391
+ return [];
8392
+ }
8393
+ return list.type === "argument_list" ? children(list) : [list];
8394
+ }
8395
+ function stopsTheField(values, input) {
8396
+ const first = values[0];
8397
+ if (first === void 0) {
8398
+ return true;
8399
+ }
8400
+ const listHead = input.hooks.answerIsListHead === true;
8401
+ if (values.length > 1) {
8402
+ return listHead && isFalsy(evaluatedValue(first, input.facts));
8403
+ }
8404
+ const value = evaluatedValue(first, input.facts);
8405
+ if (value.kind !== "sequence") {
8406
+ return isFalsy(value);
8407
+ }
8408
+ const head = value.items[0];
8409
+ return listHead && head !== void 0 && isFalsy(head.value);
8410
+ }
8411
+ function isFalsy(value) {
8412
+ const settled = constantOf4(value);
8413
+ return settled === false || settled === null;
8414
+ }
8415
+ function emptyBodyBranch(location) {
8416
+ return {
8417
+ conditions: [],
8418
+ terminal: endingTerminal("return", location),
8419
+ effects: [],
8420
+ location,
8421
+ isDefault: true
8422
+ };
8423
+ }
8424
+ function goOn(location) {
8425
+ return endingTerminal("delegate", location);
8426
+ }
8427
+ function endingTerminal(kind, location) {
8428
+ return {
8429
+ kind,
8430
+ statusCode: null,
8431
+ body: null,
8432
+ exceptionType: null,
8433
+ message: null,
8434
+ component: null,
8435
+ renderTree: null,
8436
+ delegateTarget: null,
8437
+ emitEvent: null,
8438
+ location
8439
+ };
8440
+ }
8441
+
8442
+ // src/streamWrites.ts
8443
+ import { ioBinding } from "@suss/behavioral-ir";
8444
+ import {
8445
+ callSiteKey,
8446
+ numberRepeatedCallSites,
8447
+ SKIP_CHILDREN as SKIP_CHILDREN4,
8448
+ walkDescendants as walkDescendants4
8449
+ } from "@suss/extractor";
8450
+ var RUBY_STREAM_RECOGNITION = "ruby-stdlib";
8451
+ var KERNEL_WRITES = /* @__PURE__ */ new Map([
8452
+ ["puts", "stdout"],
8453
+ ["print", "stdout"],
8454
+ ["warn", "stderr"],
8455
+ ["abort", "stderr"]
8456
+ ]);
8457
+ var STREAM_RECEIVERS = /* @__PURE__ */ new Map([
8458
+ ["$stdout", "stdout"],
8459
+ ["STDOUT", "stdout"],
8460
+ ["$stderr", "stderr"],
8461
+ ["STDERR", "stderr"]
8462
+ ]);
8463
+ var STREAM_METHODS = /* @__PURE__ */ new Set(["puts", "print", "write", "<<"]);
8464
+ var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
8465
+ function streamWriteEffects(root) {
8466
+ return numberRepeatedCallSites(
8467
+ writesUnder2(root).map((write) => [write])
8468
+ ).flat();
8469
+ }
8470
+ function writesUnder2(root) {
8471
+ const found = [];
8472
+ walkDescendants4(root, null, children, {
8473
+ at: (node) => {
8474
+ const write = node.type === "call" ? streamWriteAt(node) : null;
8475
+ if (write !== null) {
8476
+ found.push({ effect: write, at: node });
8477
+ }
8478
+ },
8479
+ into: (node) => DEFERRED_BODY_TYPES2.has(node.type) ? SKIP_CHILDREN4 : null
8194
8480
  });
8195
8481
  return found;
8196
8482
  }
@@ -8219,13 +8505,14 @@ function streamWriteAt(call) {
8219
8505
  function writeEffect(call, target, args) {
8220
8506
  const receiver = field(call, "receiver");
8221
8507
  const method = field(call, "method")?.text ?? "";
8222
- return {
8508
+ const effect = {
8223
8509
  type: "interaction",
8224
8510
  binding: ioBinding({ recognition: RUBY_STREAM_RECOGNITION, target }),
8225
8511
  callee: receiver === null ? method : `${receiver.text}.${method}`,
8226
- groupId: `${call.startPosition.row + 1}:${call.startPosition.column + 1}`,
8227
- interaction: { class: "stream-write", ...payloadOf(args) }
8512
+ interaction: { class: "stream-write", ...payloadOf(args) },
8513
+ location: rangeOf(call)
8228
8514
  };
8515
+ return { ...effect, groupId: callSiteKey(effect) };
8229
8516
  }
8230
8517
  function payloadOf(args) {
8231
8518
  const only = args[0];
@@ -9305,16 +9592,19 @@ function createFileCache(parse, readFile) {
9305
9592
  }
9306
9593
  };
9307
9594
  }
9308
- function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
9595
+ function fieldReadContext(pattern, cache, fileBlocks, options) {
9596
+ const facts = options.facts;
9597
+ const parsedFile = (absPath) => cache.get(absPath);
9309
9598
  return {
9310
9599
  pattern,
9311
9600
  cache,
9312
- bodyRead,
9601
+ bodyRead: options,
9313
9602
  facts,
9603
+ file: options.absoluteFile ?? options.filePath,
9314
9604
  read: {
9315
9605
  facts,
9316
- bodyBlocks: bodyRead.bodyBlocks,
9317
- dynamicNames: bodyRead.dynamicNames
9606
+ bodyBlocks: options.bodyBlocks,
9607
+ dynamicNames: options.dynamicNames
9318
9608
  },
9319
9609
  lookup: {
9320
9610
  root: pattern.root,
@@ -9323,8 +9613,9 @@ function fieldReadContext(pattern, cache, fileBlocks, bodyRead, facts) {
9323
9613
  acronyms: pattern.acronyms ?? [],
9324
9614
  ancestryRootClassNames: pattern.ancestryRootClassNames,
9325
9615
  constantFiles: cache.constantFiles,
9326
- parsedFile: (absPath) => cache.get(absPath),
9327
- localDefinition: (name) => sameFileBlocks(name, fileBlocks)
9616
+ parsedFile,
9617
+ localDefinition: (name) => sameFileBlocks(name, fileBlocks),
9618
+ ...facts === void 0 ? {} : { definedInRun: classesDeclaredInRun(facts, parsedFile) }
9328
9619
  }
9329
9620
  };
9330
9621
  }
@@ -9433,13 +9724,7 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
9433
9724
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
9434
9725
  return [];
9435
9726
  }
9436
- const ctx = fieldReadContext(
9437
- pattern,
9438
- options.cache,
9439
- fileBlocks,
9440
- options,
9441
- options.facts
9442
- );
9727
+ const ctx = fieldReadContext(pattern, options.cache, fileBlocks, options);
9443
9728
  const ancestry = await ancestryOf(info.qualifiedName, ownBlocks, ctx.lookup);
9444
9729
  if (!reachesConfiguredBase(ancestry, info.qualifiedName, pattern.baseClassNames)) {
9445
9730
  return [];
@@ -9460,13 +9745,47 @@ async function graphqlObjectFieldUnits(pattern, pack, info, ownBlocks, fileBlock
9460
9745
  declsByName.set(decl.fieldName, decl);
9461
9746
  }
9462
9747
  }
9463
- return [...declsByName.values()].map((decl) => {
9464
- const raw = buildFieldUnit(pack, typeName, decl, options.filePath);
9748
+ const decls = [...declsByName.values()];
9749
+ const fields = decls.map((decl) => {
9750
+ const raw = buildFieldUnit(pack, typeName, decl, options);
9465
9751
  if (decl.body.reachSeed !== void 0) {
9466
9752
  options.onReachSeed?.(raw, decl.body.reachSeed);
9467
9753
  }
9468
9754
  return raw;
9469
9755
  });
9756
+ return [...fields, ...hookUnitsOf(decls, pattern, options)];
9757
+ }
9758
+ function hookUnitsOf(decls, pattern, options) {
9759
+ const hooks = pattern.resolverHooks;
9760
+ if (hooks === void 0) {
9761
+ return [];
9762
+ }
9763
+ const units = /* @__PURE__ */ new Map();
9764
+ for (const hook of decls.flatMap((decl) => decl.hooks)) {
9765
+ const key = nodeId(hook.block.file, hook.method);
9766
+ if (units.has(key)) {
9767
+ continue;
9768
+ }
9769
+ const raw = hookUnit(hook, {
9770
+ hooks,
9771
+ body: bodyOfMethod(hook.method, hook.block.file, options),
9772
+ displayPath: displayPathIn(options, hook.block.file),
9773
+ facts: options.facts
9774
+ });
9775
+ units.set(key, raw);
9776
+ options.onReachSeed?.(raw, {
9777
+ file: hook.block.file,
9778
+ node: hook.method,
9779
+ enclosingQualifiedName: hook.block.info.qualifiedName
9780
+ });
9781
+ }
9782
+ return [...units.values()];
9783
+ }
9784
+ function displayPathIn(options, file) {
9785
+ if (file === (options.absoluteFile ?? options.filePath)) {
9786
+ return options.filePath;
9787
+ }
9788
+ return options.displayPathOf?.(file) ?? file;
9470
9789
  }
9471
9790
  async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks, options) {
9472
9791
  if (info.bodyNode === null || pattern.baseClassNames.includes(info.qualifiedName)) {
@@ -9543,7 +9862,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9543
9862
  raised: raisedStatusBranches(
9544
9863
  pattern,
9545
9864
  options.storage?.patterns ?? [],
9546
- body.extraEffects,
9865
+ everyEffect(body.extraEffects),
9547
9866
  "eachRoute",
9548
9867
  rangeOf(filter.method)
9549
9868
  )
@@ -9576,10 +9895,7 @@ async function controllerActionUnits(pattern, pack, info, ownBlocks, fileBlocks,
9576
9895
  actionName,
9577
9896
  route,
9578
9897
  method,
9579
- {
9580
- display: block.file === (options.absoluteFile ?? options.filePath) ? options.filePath : options.displayPathOf?.(block.file) ?? block.file,
9581
- absolute: block.file
9582
- },
9898
+ { display: displayPathIn(options, block.file), absolute: block.file },
9583
9899
  options,
9584
9900
  around,
9585
9901
  respondingHelper,
@@ -9748,8 +10064,8 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
9748
10064
  reading: absentReading3,
9749
10065
  libraryDefault: pattern.defaultStatusCode
9750
10066
  },
9751
- effects: body.effects ?? [],
9752
- ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
10067
+ effects: everyEffect(body.effects),
10068
+ ...body.extraEffects === void 0 ? {} : { extraEffects: everyEffect(body.extraEffects) },
9753
10069
  location: range,
9754
10070
  isDefault: true
9755
10071
  }
@@ -9759,7 +10075,7 @@ function buildControllerActionUnit(pack, pattern, controllerQualifiedName, actio
9759
10075
  raisedStatusBranches(
9760
10076
  pattern,
9761
10077
  bodyRead.storage?.patterns ?? [],
9762
- body.extraEffects,
10078
+ everyEffect(body.extraEffects),
9763
10079
  raises2.handlers,
9764
10080
  range
9765
10081
  )
@@ -9804,7 +10120,7 @@ function routingGapUnit(pattern, gaps) {
9804
10120
  isDefault: true
9805
10121
  }
9806
10122
  ],
9807
- readings: gaps.map((gap) => unreadableReading2(gap, range)),
10123
+ readings: gaps.map((gap) => unreadableReading3(gap, range)),
9808
10124
  dependencyCalls: [],
9809
10125
  declaredContract: null
9810
10126
  };
@@ -9838,15 +10154,18 @@ function branchesFor(body, range) {
9838
10154
  emitEvent: null,
9839
10155
  location: range
9840
10156
  },
9841
- effects: body.effects ?? [],
9842
- ...body.extraEffects === void 0 ? {} : { extraEffects: body.extraEffects },
10157
+ effects: everyEffect(body.effects),
10158
+ ...body.extraEffects === void 0 ? {} : { extraEffects: everyEffect(body.extraEffects) },
9843
10159
  location: range,
9844
10160
  isDefault: true
9845
10161
  };
9846
10162
  return withSlotSources([branch], body.provenance);
9847
10163
  }
10164
+ function everyEffect(placed) {
10165
+ return (placed ?? []).map((one) => one.effect);
10166
+ }
9848
10167
  function bodyOfMethod(method, file, bodyRead = {}) {
9849
- const effects = invocationEffects(
10168
+ const effects = invocationSites(
9850
10169
  method,
9851
10170
  bodyRead.inheritedMethods,
9852
10171
  EVERY_ARGLESS_CALL,
@@ -9855,7 +10174,7 @@ function bodyOfMethod(method, file, bodyRead = {}) {
9855
10174
  const storage = bodyRead.storage;
9856
10175
  const facts = bodyRead.facts;
9857
10176
  const database = storage === void 0 ? { effects: [], slots: [] } : methodStorage(method, file, storage);
9858
- const extra = [
10177
+ const found = [
9859
10178
  ...envReadEffects(
9860
10179
  method,
9861
10180
  facts === void 0 ? void 0 : { db: facts, file }
@@ -9863,6 +10182,11 @@ function bodyOfMethod(method, file, bodyRead = {}) {
9863
10182
  ...database.effects,
9864
10183
  ...streamWriteEffects(method)
9865
10184
  ];
10185
+ const siteOf3 = effectSitesIn(
10186
+ methodBody(method),
10187
+ found.map((one) => one.at)
10188
+ );
10189
+ const extra = found.map(({ effect, at }) => ({ effect, site: siteOf3(at) }));
9866
10190
  const provenance = database.slots.length === 0 || storage === void 0 ? [] : slotProvenance(database.slots, { facts: storage.facts, unit: method });
9867
10191
  return {
9868
10192
  bodyContent: methodHasStatements(method) ? "statements" : "empty",
@@ -9877,7 +10201,7 @@ var NO_METHOD_BEHIND_IT = {
9877
10201
  readings: []
9878
10202
  };
9879
10203
  function methodNotSettled(reason, range) {
9880
- return { readings: [unreadableReading2(reason, range)] };
10204
+ return { readings: [unreadableReading3(reason, range)] };
9881
10205
  }
9882
10206
  function bodyFromLookup(found, range, subject, nothingThere, bodyRead) {
9883
10207
  const table = {
@@ -9926,7 +10250,8 @@ async function readFieldCall(stmt, scope, ctx, ancestry) {
9926
10250
  namedOnTheWire: true,
9927
10251
  node: stmt,
9928
10252
  contract: read.contract,
9929
- body: read.body
10253
+ body: read.body,
10254
+ hooks: read.hooks ?? []
9930
10255
  };
9931
10256
  }
9932
10257
  function computedNameDeclaration(stmt, nameArg) {
@@ -9936,9 +10261,10 @@ function computedNameDeclaration(stmt, nameArg) {
9936
10261
  namedOnTheWire: false,
9937
10262
  node: stmt,
9938
10263
  contract: null,
10264
+ hooks: [],
9939
10265
  body: {
9940
10266
  readings: [
9941
- unreadableReading2(
10267
+ unreadableReading3(
9942
10268
  `This field is named by ${nameArg.text}, which is worked out when the class body runs, so the name the schema exposes it under was not read here`,
9943
10269
  range
9944
10270
  )
@@ -9962,14 +10288,38 @@ async function readFieldShape(symbol, callArgs, scope, ctx, ancestry, range) {
9962
10288
  }
9963
10289
  return {
9964
10290
  contract: literalContract(callArgs, scope, ctx),
9965
- body: bodyFromLookup(
10291
+ body: methodBehindField(symbol, callArgs, ctx, ancestry, range)
10292
+ };
10293
+ }
10294
+ function methodBehindField(symbol, callArgs, ctx, ancestry, range) {
10295
+ const keyword = ctx.pattern.resolverMethodKeyword;
10296
+ const written = keyword === void 0 ? void 0 : callArgs.keyword[keyword];
10297
+ if (written === void 0) {
10298
+ return bodyFromLookup(
9966
10299
  methodInAncestry(ancestry, symbol, ctx.read),
9967
10300
  range,
9968
10301
  "This field",
9969
10302
  NO_METHOD_BEHIND_IT,
9970
10303
  ctx.bodyRead
9971
- )
9972
- };
10304
+ );
10305
+ }
10306
+ const name = stringValueOf(written, ctx.facts);
10307
+ if (name === null) {
10308
+ return methodNotSettled(
10309
+ `This field is resolved by the method ${written.text}, which is worked out at run time, so nothing about what it does was read here`,
10310
+ range
10311
+ );
10312
+ }
10313
+ return bodyFromLookup(
10314
+ methodInAncestry(ancestry, name, ctx.read),
10315
+ range,
10316
+ "This field",
10317
+ methodNotSettled(
10318
+ `This field is resolved by ${name}, which nothing in its type's ancestry defines, so nothing about what it does was read here`,
10319
+ range
10320
+ ),
10321
+ ctx.bodyRead
10322
+ );
9973
10323
  }
9974
10324
  function literalContract(callArgs, scope, ctx) {
9975
10325
  const typeArg = callArgs.positional[1];
@@ -9985,21 +10335,12 @@ function literalContract(callArgs, scope, ctx) {
9985
10335
  async function readWiredClass(ref, scope, ctx) {
9986
10336
  const range = rangeOf(ref);
9987
10337
  const candidates = constantRefCandidates(ref, scope.nesting);
9988
- if (candidates.length === 0) {
9989
- return {
9990
- contract: null,
9991
- body: methodNotSettled(
9992
- `This field is wired to ${ref.text}, which is not a constant path this reader follows, so nothing about what it does was read here`,
9993
- range
9994
- )
9995
- };
9996
- }
9997
- const reached = await reachConstant(candidates, ctx.lookup);
10338
+ const reached = await wiredClassInRun(ref, ctx) ?? (candidates.length === 0 ? null : await reachConstant(candidates, ctx.lookup));
9998
10339
  if (reached === null) {
9999
10340
  return {
10000
10341
  contract: null,
10001
10342
  body: methodNotSettled(
10002
- `This field is wired to ${ref.text}, which this run did not read, so nothing about what it does was read here`,
10343
+ candidates.length === 0 ? `This field is wired to ${ref.text}, which is not a constant path this reader follows, so nothing about what it does was read here` : `This field is wired to ${ref.text}, which this run did not read, so nothing about what it does was read here`,
10003
10344
  range
10004
10345
  )
10005
10346
  };
@@ -10010,20 +10351,44 @@ async function readWiredClass(ref, scope, ctx) {
10010
10351
  reached.blocks,
10011
10352
  ctx.lookup
10012
10353
  );
10354
+ const body = bodyFromLookup(
10355
+ methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
10356
+ range,
10357
+ `This field's ${targetQualifiedName}`,
10358
+ methodNotSettled(
10359
+ `This field is wired to ${targetQualifiedName}, which defines no ${ctx.pattern.resolverMethodName} method anywhere in its ancestry, so nothing about what it does was read here`,
10360
+ range
10361
+ ),
10362
+ ctx.bodyRead
10363
+ );
10364
+ const hooks = ctx.pattern.resolverHooks === void 0 ? { definitions: [], unsettled: [] } : hooksRun(ancestry, ctx.pattern.resolverHooks, ctx.read);
10013
10365
  return {
10014
10366
  contract: readClassContract(ancestry, ctx.pattern),
10015
- body: bodyFromLookup(
10016
- methodInAncestry(ancestry, ctx.pattern.resolverMethodName, ctx.read),
10017
- range,
10018
- `This field's ${targetQualifiedName}`,
10019
- methodNotSettled(
10020
- `This field is wired to ${targetQualifiedName}, which defines no ${ctx.pattern.resolverMethodName} method anywhere in its ancestry, so nothing about what it does was read here`,
10021
- range
10022
- ),
10023
- ctx.bodyRead
10024
- )
10367
+ body: {
10368
+ ...body,
10369
+ readings: [
10370
+ ...body.readings,
10371
+ ...hooks.unsettled.map(
10372
+ (one) => unsettledHookReading(targetQualifiedName, one, range)
10373
+ )
10374
+ ]
10375
+ },
10376
+ hooks: hooks.definitions
10025
10377
  };
10026
10378
  }
10379
+ async function wiredClassInRun(ref, ctx) {
10380
+ const inRun = ctx.lookup.definedInRun;
10381
+ if (ctx.facts === void 0 || inRun === void 0) {
10382
+ return null;
10383
+ }
10384
+ const classKey = classSettledOn(ctx.facts, readKey(ctx.file, ref, null));
10385
+ const name = classKey === void 0 ? void 0 : declaredName(ctx.facts, classKey);
10386
+ if (name === void 0) {
10387
+ return null;
10388
+ }
10389
+ const blocks = await inRun(name);
10390
+ return blocks === null ? null : { name, blocks };
10391
+ }
10027
10392
  function readTypeCall(callArgs, scope, pattern, out) {
10028
10393
  const typeArg = callArgs.positional[0];
10029
10394
  if (typeArg !== void 0) {
@@ -10141,7 +10506,11 @@ function toCamelCase(name) {
10141
10506
  ...rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
10142
10507
  ].join("");
10143
10508
  }
10144
- function buildFieldUnit(pack, typeName, decl, filePath) {
10509
+ function buildFieldUnit(pack, typeName, decl, options) {
10510
+ const filePath = options.filePath;
10511
+ const wrappers = decl.hooks.map(
10512
+ (hook) => hookReference(hook, displayPathIn(options, hook.block.file))
10513
+ );
10145
10514
  const parameters = (decl.contract?.methodArgs ?? decl.contract?.args ?? []).map((arg, position) => ({
10146
10515
  name: arg.name,
10147
10516
  position,
@@ -10181,7 +10550,8 @@ function buildFieldUnit(pack, typeName, decl, filePath) {
10181
10550
  dependencyCalls: [],
10182
10551
  declaredContract: null,
10183
10552
  ...decl.body.readings.length > 0 ? { readings: decl.body.readings } : {},
10184
- ...graphqlDeclaredContract !== void 0 ? { graphqlDeclaredContract } : {}
10553
+ ...graphqlDeclaredContract !== void 0 ? { graphqlDeclaredContract } : {},
10554
+ ...wrappers.length > 0 ? { wrappers } : {}
10185
10555
  };
10186
10556
  }
10187
10557
 
@@ -10649,14 +11019,39 @@ function buildRubyExtractionReport(args) {
10649
11019
  });
10650
11020
  }
10651
11021
 
11022
+ // src/reach/closure.ts
11023
+ import {
11024
+ functionCallBinding,
11025
+ placeArgTargets,
11026
+ placeCalleeParameters,
11027
+ placeCalls,
11028
+ placePossibleCalls,
11029
+ ReceiverClasses,
11030
+ recordParameterGaps,
11031
+ StopList,
11032
+ sameDeclarations,
11033
+ TargetPlacements,
11034
+ unfollowedCallGap,
11035
+ worthRecording
11036
+ } from "@suss/behavioral-ir";
11037
+ import { Database, evaluate, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
11038
+ import { assembleSummary } from "@suss/extractor";
11039
+ import {
11040
+ fileOfKey as fileOfKey2,
11041
+ keptVariants,
11042
+ noDependencies,
11043
+ plainKeyOf,
11044
+ variantKey
11045
+ } from "@suss/resolution";
11046
+
10652
11047
  // src/exits.ts
10653
11048
  import {
10654
11049
  markReturnsAsExitCode,
10655
- SKIP_CHILDREN as SKIP_CHILDREN4,
10656
- walkDescendants as walkDescendants4
11050
+ SKIP_CHILDREN as SKIP_CHILDREN5,
11051
+ walkDescendants as walkDescendants5
10657
11052
  } from "@suss/extractor";
10658
11053
  import { exitCodeFunctions } from "@suss/resolution";
10659
- import { constantOf as constantOf4 } from "@suss/values";
11054
+ import { constantOf as constantOf5 } from "@suss/values";
10660
11055
  var EXIT_METHODS = /* @__PURE__ */ new Map([
10661
11056
  ["exit", 0],
10662
11057
  ["exit!", 1],
@@ -10665,14 +11060,14 @@ var EXIT_METHODS = /* @__PURE__ */ new Map([
10665
11060
  var DEFERRED_BODY_TYPES3 = /* @__PURE__ */ new Set(["method", "singleton_method", "lambda"]);
10666
11061
  function exitSites(root) {
10667
11062
  const found = [];
10668
- walkDescendants4(root, null, children, {
11063
+ walkDescendants5(root, null, children, {
10669
11064
  at: (node) => {
10670
11065
  const site = node.type === "call" ? exitSiteAt(node) : null;
10671
11066
  if (site !== null) {
10672
11067
  found.push(site);
10673
11068
  }
10674
11069
  },
10675
- into: (node) => DEFERRED_BODY_TYPES3.has(node.type) ? SKIP_CHILDREN4 : null
11070
+ into: (node) => DEFERRED_BODY_TYPES3.has(node.type) ? SKIP_CHILDREN5 : null
10676
11071
  });
10677
11072
  return found;
10678
11073
  }
@@ -10709,7 +11104,7 @@ function exitCodeOf(site, facts) {
10709
11104
  if (written !== void 0) {
10710
11105
  return { type: "literal", value: written };
10711
11106
  }
10712
- const value = constantOf4(evaluatedValue(code, facts));
11107
+ const value = constantOf5(evaluatedValue(code, facts));
10713
11108
  return typeof value === "number" ? { type: "literal", value } : { type: "dynamic", sourceText: code.text };
10714
11109
  }
10715
11110
  function exitingBranches(method, effects, facts) {
@@ -10737,105 +11132,6 @@ function markExitCodeFunctions(summaries, exits, facts, workspaceRoot) {
10737
11132
  );
10738
11133
  }
10739
11134
 
10740
- // src/facts/callbacks.ts
10741
- function callbacksIn(patterns) {
10742
- return patterns.flatMap(
10743
- (pattern) => pattern.callbacks === void 0 ? [] : [pattern.callbacks]
10744
- );
10745
- }
10746
- function emitClassCallbacks(db, file, classNode, declared) {
10747
- const body = field(classNode, "body");
10748
- if (body === null || declared.length === 0) {
10749
- return;
10750
- }
10751
- const classKey = nodeId(file, classNode);
10752
- for (const statement of runStatements(body)) {
10753
- if (statement.type !== "call" || field(statement, "receiver") !== null) {
10754
- continue;
10755
- }
10756
- const called = field(statement, "method")?.text ?? "";
10757
- const args = readCallArgs(field(statement, "arguments"));
10758
- for (const callbacks of declared) {
10759
- const events = eventsOf(args, called, callbacks, db);
10760
- for (const event of events) {
10761
- for (const method of symbolArgumentNames(args, db)) {
10762
- db.add("classCallback", [classKey, event, method]);
10763
- }
10764
- }
10765
- }
10766
- }
10767
- }
10768
- function eventsOf(args, called, callbacks, db) {
10769
- const covered = callbacks.registeredBy[called];
10770
- if (covered === void 0) {
10771
- return [];
10772
- }
10773
- const written = args.keyword[callbacks.eventKeyword];
10774
- const narrowed = written === void 0 ? null : stringValueOf(written, db);
10775
- if (narrowed === null) {
10776
- return covered;
10777
- }
10778
- return covered.filter((event) => event === narrowed);
10779
- }
10780
-
10781
- // src/facts.ts
10782
- import path3 from "path";
10783
- import { SKIP_CHILDREN as SKIP_CHILDREN5, walkDescendants as walkDescendants5 } from "@suss/extractor";
10784
- function emitRequireFacts(db, filePath, root, known) {
10785
- walkDescendants5(root, null, children, {
10786
- at: (node) => {
10787
- const target = requiredRelativePath(node);
10788
- if (target === null) {
10789
- return;
10790
- }
10791
- const resolved = path3.resolve(
10792
- path3.dirname(filePath),
10793
- target.endsWith(".rb") ? target : `${target}.rb`
10794
- );
10795
- if (known.has(resolved)) {
10796
- db.add("importsFile", [filePath, resolved]);
10797
- }
10798
- },
10799
- into: (node) => node.type === "argument_list" ? SKIP_CHILDREN5 : null
10800
- });
10801
- }
10802
- function requiredRelativePath(node) {
10803
- if (node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== "require_relative") {
10804
- return null;
10805
- }
10806
- const { positional } = readCallArgs(field(node, "arguments"));
10807
- return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
10808
- }
10809
-
10810
- // src/moduleSurface.ts
10811
- import { moduleExportBinding } from "@suss/behavioral-ir";
10812
- import {
10813
- MODULE_SURFACE_RECOGNITION,
10814
- moduleOfFile,
10815
- settleModules
10816
- } from "@suss/extractor";
10817
-
10818
- // src/reach/closure.ts
10819
- import {
10820
- functionCallBinding,
10821
- placeArgTargets,
10822
- placeCalleeParameters,
10823
- placeCalls,
10824
- placePossibleCalls,
10825
- ReceiverClasses,
10826
- recordParameterGaps,
10827
- StopList,
10828
- TargetPlacements,
10829
- unfollowedCallGap,
10830
- worthRecording
10831
- } from "@suss/behavioral-ir";
10832
- import { Database, evaluate, lit as lit2, rule as rule2, variable as v2 } from "@suss/datalog";
10833
- import { assembleSummary } from "@suss/extractor";
10834
- import {
10835
- fileOfKey as fileOfKey2,
10836
- noDependencies
10837
- } from "@suss/resolution";
10838
-
10839
11135
  // src/flags.ts
10840
11136
  import { SKIP_CHILDREN as SKIP_CHILDREN6, walkDescendants as walkDescendants6 } from "@suss/extractor";
10841
11137
  var PARSE_METHODS = /* @__PURE__ */ new Set(["parse", "parse!", "permute", "permute!"]);
@@ -10941,8 +11237,14 @@ import {
10941
11237
  calleeOutcomes,
10942
11238
  classMemberName as classMemberName3,
10943
11239
  couldBeSettled,
10944
- dispatchOutcomes
11240
+ dispatchOutcomes,
11241
+ SELF_AS_CLASS_NAME as SELF_AS_CLASS_NAME2,
11242
+ selfFormOf,
11243
+ selfResultOf
10945
11244
  } from "@suss/resolution";
11245
+ function selfClassOf(site) {
11246
+ return site.receiverClass?.name ?? site.enclosingQualifiedName;
11247
+ }
10946
11248
  var STOP_FOR = {
10947
11249
  severalSources: "multipleSources",
10948
11250
  outsideRun: "outsideRun",
@@ -11139,6 +11441,24 @@ function asCallee(spelling, outcome, site, ctx) {
11139
11441
  if (objects.length > 1) {
11140
11442
  return stop("multipleSources");
11141
11443
  }
11444
+ const fromSelf = ownReceiverForm(spelling.key, site, ctx);
11445
+ const selfClass = selfClassOf(site);
11446
+ if (fromSelf !== null && selfClass !== null) {
11447
+ return methodOfClass(
11448
+ { ...spelling, onClassItself: fromSelf === SELF_AS_CLASS_NAME2 },
11449
+ selfClass,
11450
+ ctx
11451
+ );
11452
+ }
11453
+ const returned = selfResultOf(ctx.facts, spelling.key);
11454
+ const sentTo = returned === null ? null : classSentTo(returned.receiver, site, ctx);
11455
+ if (returned !== null && sentTo !== null) {
11456
+ return methodOfClass(
11457
+ { ...spelling, onClassItself: returned.form === SELF_AS_CLASS_NAME2 },
11458
+ sentTo,
11459
+ ctx
11460
+ );
11461
+ }
11142
11462
  const objectKey = objects[0] ?? (outcome.kind === "object" ? outcome.key : void 0);
11143
11463
  if (objectKey !== void 0) {
11144
11464
  return methodOnObject(spelling, objectKey, ctx);
@@ -11148,6 +11468,17 @@ function asCallee(spelling, outcome, site, ctx) {
11148
11468
  }
11149
11469
  return stop(STOP_FOR[outcome.kind] ?? "noDeclaration");
11150
11470
  }
11471
+ function ownReceiverForm(key, site, ctx) {
11472
+ return site.method === null ? null : selfFormOf(ctx.facts, key, receiverKeyOf(site.file, site.method));
11473
+ }
11474
+ function classSentTo(receiver, site, ctx) {
11475
+ resolveValues(ctx.facts, [receiver]);
11476
+ if (ownReceiverForm(receiver, site, ctx) !== null) {
11477
+ return selfClassOf(site);
11478
+ }
11479
+ const [only, ...more] = objectsBehind(ctx.facts, receiver);
11480
+ return only === void 0 || more.length > 0 ? null : ctx.classNames.get(only) ?? null;
11481
+ }
11151
11482
  function objectsBehind(facts, key) {
11152
11483
  return answersFor2(facts, "wantedObjectOf", key);
11153
11484
  }
@@ -11165,11 +11496,18 @@ function methodOfClass(spelling, qualifiedName, ctx) {
11165
11496
  if (!spelling.onClassItself) {
11166
11497
  return methodOnAncestryOf(qualifiedName, spelling.method, ctx);
11167
11498
  }
11168
- if (spelling.method === "new") {
11169
- return methodOnAncestryOf(qualifiedName, "initialize", ctx);
11499
+ if (spelling.method === CONSTRUCTOR) {
11500
+ return methodOnAncestryOf(qualifiedName, INITIALIZE_METHOD, ctx);
11501
+ }
11502
+ return classMethodOn(qualifiedName, spelling.method, ctx);
11503
+ }
11504
+ function classMethodOn(qualifiedName, method, ctx) {
11505
+ const written = singletonMethodOn(qualifiedName, method, ctx);
11506
+ if (written.kind === "followed") {
11507
+ return written;
11170
11508
  }
11171
- const written = singletonMethodOn(qualifiedName, spelling.method, ctx);
11172
- return written.kind === "followed" ? written : scopeOn(qualifiedName, spelling.method, ctx) ?? written;
11509
+ const inherited = scopeOn(qualifiedName, method, ctx);
11510
+ return inherited === null ? written : followed(runningOn(qualifiedName, inherited, ctx));
11173
11511
  }
11174
11512
  function scopeOn(qualifiedName, method, ctx) {
11175
11513
  const spelled = classMemberName3(method);
@@ -11181,7 +11519,7 @@ function scopeOn(qualifiedName, method, ctx) {
11181
11519
  for (const row of ctx.facts.lookup("holdsProperty", 0, classKey)) {
11182
11520
  const target = row[1] === spelled ? ctx.definitions.get(String(row[2])) : void 0;
11183
11521
  if (target !== void 0) {
11184
- return followed(target);
11522
+ return target;
11185
11523
  }
11186
11524
  }
11187
11525
  }
@@ -11205,7 +11543,8 @@ function leavesRoomForATopLevelMethod(reason) {
11205
11543
  return reason === "noDeclaration" || reason === "outsideRun";
11206
11544
  }
11207
11545
  function resolveImplicitSelf(methodName, site, ctx) {
11208
- const onClass = site.enclosingQualifiedName === null ? null : methodOnSelf(site, site.enclosingQualifiedName, methodName, ctx);
11546
+ const selfClass = selfClassOf(site);
11547
+ const onClass = selfClass === null ? null : methodOnSelf(site, selfClass, methodName, ctx);
11209
11548
  if (onClass !== null && (onClass.kind === "followed" || !leavesRoomForATopLevelMethod(onClass.reason))) {
11210
11549
  return onClass;
11211
11550
  }
@@ -11220,7 +11559,7 @@ function resolveImplicitSelf(methodName, site, ctx) {
11220
11559
  return readWholeAncestry && !OBJECT_METHODS.has(methodName) && inModuleInstanceMethod(site, ctx) ? SELF_CALL_IN_MIXIN : UNSETTLED_ON_SELF;
11221
11560
  }
11222
11561
  function inModuleInstanceMethod(site, ctx) {
11223
- const qualifiedName = site.enclosingQualifiedName;
11562
+ const qualifiedName = selfClassOf(site);
11224
11563
  if (qualifiedName === null || site.method === null || inClassMethod(site, ctx)) {
11225
11564
  return false;
11226
11565
  }
@@ -11228,7 +11567,7 @@ function inModuleInstanceMethod(site, ctx) {
11228
11567
  return own?.type === "bodies" && own.blocks.every((block) => block.info.kind === "module");
11229
11568
  }
11230
11569
  function declaredForTheOtherSelf(site, methodName, ctx) {
11231
- const qualifiedName = site.enclosingQualifiedName;
11570
+ const qualifiedName = selfClassOf(site);
11232
11571
  if (qualifiedName === null) {
11233
11572
  return false;
11234
11573
  }
@@ -11239,10 +11578,10 @@ function inClassMethod(site, ctx) {
11239
11578
  return site.method !== null && definesClassMethod(site.method, ctx.bodyBlocks);
11240
11579
  }
11241
11580
  function methodOnSelf(site, qualifiedName, methodName, ctx) {
11242
- if (inClassMethod(site, ctx)) {
11243
- return singletonMethodOn(qualifiedName, methodName, ctx);
11581
+ if (!inClassMethod(site, ctx)) {
11582
+ return methodOnAncestryOf(qualifiedName, methodName, ctx);
11244
11583
  }
11245
- return methodOnAncestryOf(qualifiedName, methodName, ctx);
11584
+ return methodName === CONSTRUCTOR ? methodOnAncestryOf(qualifiedName, INITIALIZE_METHOD, ctx) : classMethodOn(qualifiedName, methodName, ctx);
11246
11585
  }
11247
11586
  function resolveTopLevelName(methodName, ctx) {
11248
11587
  const candidates = ctx.topLevelMethods.get(methodName);
@@ -11265,14 +11604,37 @@ function methodOnAncestryOf(qualifiedName, methodName, ctx) {
11265
11604
  dynamicNames: ctx.dynamicNames
11266
11605
  });
11267
11606
  if (found.type === "found") {
11268
- return followed(reachedMethod(found.method, found.block, methodName));
11607
+ return followed(
11608
+ runningOn(
11609
+ qualifiedName,
11610
+ reachedMethod(found.method, found.block, methodName),
11611
+ ctx
11612
+ )
11613
+ );
11269
11614
  }
11270
11615
  if (found.type === "unsettled") {
11271
- return stop(unsettledReason(found));
11616
+ return stop(unsettledReason2(found));
11272
11617
  }
11273
11618
  return NO_DECLARATION;
11274
11619
  }
11275
- function unsettledReason(found) {
11620
+ function runningOn(qualifiedName, target, ctx) {
11621
+ if (target.enclosingQualifiedName === qualifiedName) {
11622
+ return target;
11623
+ }
11624
+ const own = (ctx.ancestries.get(qualifiedName) ?? []).find(
11625
+ (entry) => entry.type === "bodies" && entry.name === qualifiedName
11626
+ );
11627
+ const file = own?.type === "bodies" ? own.blocks[0]?.file : void 0;
11628
+ if (file === void 0) {
11629
+ return target;
11630
+ }
11631
+ return {
11632
+ ...target,
11633
+ exportPath: [qualifiedName, target.name],
11634
+ receiverClass: { name: qualifiedName, file }
11635
+ };
11636
+ }
11637
+ function unsettledReason2(found) {
11276
11638
  if (found.cause === "dynamicDefine") {
11277
11639
  return "definedAtLoadTime";
11278
11640
  }
@@ -11335,11 +11697,12 @@ async function reachedFunctions(seeds, options) {
11335
11697
  const charges = /* @__PURE__ */ new Map();
11336
11698
  const beforeGaps = /* @__PURE__ */ new Map();
11337
11699
  const functionFor = (target) => {
11338
- const node = replay?.nodeOf(target.key) ?? null;
11339
- if (node === null) {
11700
+ const node = replay?.nodeOf(plainKeyOf(target.key)) ?? null;
11701
+ const found = node === null ? null : { ...target, node };
11702
+ if (found === null || keyOf(found) !== target.key) {
11340
11703
  throw new ReplayFailed(target.key);
11341
11704
  }
11342
- return { ...target, node };
11705
+ return found;
11343
11706
  };
11344
11707
  for (const seed of seeds) {
11345
11708
  seedKeys.add(seed.key);
@@ -11399,7 +11762,10 @@ async function reachedFunctions(seeds, options) {
11399
11762
  if (scan.followed.length > 0) {
11400
11763
  followedKeys.add(key);
11401
11764
  }
11402
- for (const target of scan.followed) {
11765
+ const calledHere = scan.followed.flatMap(
11766
+ (target) => target.receiverClass === void 0 ? [target] : [plainOf(target), target]
11767
+ );
11768
+ for (const target of calledHere) {
11403
11769
  const calleeKey = keyOf(target);
11404
11770
  if (!functionByKey.has(calleeKey)) {
11405
11771
  functionByKey.set(calleeKey, target);
@@ -11420,12 +11786,22 @@ async function reachedFunctions(seeds, options) {
11420
11786
  options.storage
11421
11787
  );
11422
11788
  }
11789
+ const placementsOf = (key) => ({
11790
+ targets: targetsByKey.get(key),
11791
+ argTargets: argTargetsByKey.get(key),
11792
+ possibleTargets: possibleTargetsByKey.get(key)
11793
+ });
11794
+ const onSubclass = keptVariants(
11795
+ [...functionByKey].flatMap(
11796
+ ([key, target]) => target.receiverClass === void 0 ? [] : [variantOf(key, scans, placementsOf)]
11797
+ )
11798
+ );
11423
11799
  const summaries = [];
11424
11800
  const summariesByKey = /* @__PURE__ */ new Map();
11425
11801
  for (const [keyAtom] of db.facts("reachable")) {
11426
11802
  const key = String(keyAtom);
11427
11803
  const target = functionByKey.get(key);
11428
- if (seedKeys.has(key) || target === void 0) {
11804
+ if (seedKeys.has(key) || target === void 0 || target.receiverClass !== void 0 && !onSubclass.has(key)) {
11429
11805
  continue;
11430
11806
  }
11431
11807
  const kept = replay?.summaryOf(key);
@@ -11440,6 +11816,12 @@ async function reachedFunctions(seeds, options) {
11440
11816
  const raw = libraryUnit(target, options);
11441
11817
  dropPropertyReads(raw, propertyReadsByKey.get(key));
11442
11818
  const summary2 = assembleSummary(raw, { gapHandling });
11819
+ if (target.receiverClass !== void 0) {
11820
+ summary2.receiverClass = subclassAt(
11821
+ target.receiverClass,
11822
+ options.displayPathOf
11823
+ );
11824
+ }
11443
11825
  if (gapHandling !== "silent") {
11444
11826
  summary2.gaps.push(
11445
11827
  ...(stopsByKey.get(key) ?? []).map(unfollowedCallGap)
@@ -11481,6 +11863,16 @@ async function reachedFunctions(seeds, options) {
11481
11863
  beforeGaps
11482
11864
  };
11483
11865
  }
11866
+ function variantOf(key, scans, placementsOf) {
11867
+ return {
11868
+ key,
11869
+ differs: !sameDeclarations(
11870
+ placementsOf(key),
11871
+ placementsOf(plainKeyOf(key))
11872
+ ),
11873
+ calls: scans.get(key)?.followed.map((target) => target.key) ?? []
11874
+ };
11875
+ }
11484
11876
  var ReplayFailed = class extends Error {
11485
11877
  constructor(key) {
11486
11878
  super(`The cache stored a method at ${key}, and nothing is there now.`);
@@ -11571,7 +11963,8 @@ function storedScan(scan) {
11571
11963
  file: target.file,
11572
11964
  name: target.name,
11573
11965
  exportPath: target.exportPath,
11574
- enclosingQualifiedName: target.enclosingQualifiedName
11966
+ enclosingQualifiedName: target.enclosingQualifiedName,
11967
+ ...target.receiverClass === void 0 ? {} : { receiverClass: target.receiverClass }
11575
11968
  })),
11576
11969
  stops: scan.stops,
11577
11970
  targets: [...scan.targets],
@@ -11608,7 +12001,25 @@ function scanFromStored(stored, functionFor) {
11608
12001
  };
11609
12002
  }
11610
12003
  function keyOf(target) {
11611
- return nodeId(target.file, target.node);
12004
+ const key = nodeId(target.file, target.node);
12005
+ return target.receiverClass === void 0 ? key : variantKey(key, target.receiverClass);
12006
+ }
12007
+ function plainOf(target) {
12008
+ const { receiverClass: _, ...plain } = target;
12009
+ return plain.enclosingQualifiedName === null ? plain : { ...plain, exportPath: [plain.enclosingQualifiedName, plain.name] };
12010
+ }
12011
+ function placeOf(target, displayPathOf) {
12012
+ const place = {
12013
+ file: displayPathOf(target.file),
12014
+ span: spanOf(target.node)
12015
+ };
12016
+ return target.receiverClass === void 0 ? place : {
12017
+ ...place,
12018
+ receiverClass: subclassAt(target.receiverClass, displayPathOf)
12019
+ };
12020
+ }
12021
+ function subclassAt(subclass, displayPathOf) {
12022
+ return { name: subclass.name, file: displayPathOf(subclass.file) };
11612
12023
  }
11613
12024
  function dropPropertyReads(raw, callees) {
11614
12025
  if (callees === void 0) {
@@ -11681,8 +12092,9 @@ function siteOf2(source) {
11681
12092
  return {
11682
12093
  file: source.file,
11683
12094
  method: localsOwnerOf(source.node),
11684
- owner: keyOf(source),
11685
- enclosingQualifiedName: source.enclosingQualifiedName
12095
+ owner: nodeId(source.file, source.node),
12096
+ enclosingQualifiedName: source.enclosingQualifiedName,
12097
+ receiverClass: source.receiverClass
11686
12098
  };
11687
12099
  }
11688
12100
  function localsOwnerOf(node) {
@@ -11725,17 +12137,21 @@ function scanBody(source, ctx, options, read) {
11725
12137
  const seen = /* @__PURE__ */ new Set();
11726
12138
  const parameterCallsSeen = /* @__PURE__ */ new Set();
11727
12139
  const keptArgless = /* @__PURE__ */ new Set();
11728
- const recordPassedArgs = (call, callee, calleeKey) => {
12140
+ const recordPassedArgs = (call, callee, target) => {
11729
12141
  const args = field(call, "arguments");
11730
12142
  if (args === null) {
11731
12143
  return;
11732
12144
  }
12145
+ const calleeKeys = target === null ? [] : [keyOf(target), keyOf(plainOf(target))];
12146
+ const passes = (position) => {
12147
+ for (const calleeKey of calleeKeys) {
12148
+ passedPositions.add(`${calleeKey}#${position}`);
12149
+ }
12150
+ };
11733
12151
  const argNodes = bodyStatements(args);
11734
12152
  for (let position = 0; position < argNodes.length; position += 1) {
11735
12153
  if (argNodes[position]?.type === LAMBDA_TYPE) {
11736
- if (calleeKey !== null) {
11737
- passedPositions.add(`${calleeKey}#${position}`);
11738
- }
12154
+ passes(position);
11739
12155
  continue;
11740
12156
  }
11741
12157
  const symbol = methodReferenceSymbol(argNodes[position]);
@@ -11751,13 +12167,8 @@ function scanBody(source, ctx, options, read) {
11751
12167
  seen.add(key);
11752
12168
  followed2.push(resolved);
11753
12169
  }
11754
- placements.placeArg(callee, position, {
11755
- file: displayPathOf(resolved.file),
11756
- span: spanOf(resolved.node)
11757
- });
11758
- if (calleeKey !== null) {
11759
- passedPositions.add(`${calleeKey}#${position}`);
11760
- }
12170
+ placements.placeArg(callee, position, placeOf(resolved, displayPathOf));
12171
+ passes(position);
11761
12172
  }
11762
12173
  };
11763
12174
  const followCallback = (name, key) => {
@@ -11793,10 +12204,7 @@ function scanBody(source, ctx, options, read) {
11793
12204
  dispatched.targets.forEach(follow);
11794
12205
  placements.placePossible(
11795
12206
  callee,
11796
- dispatched.targets.map((target) => ({
11797
- file: displayPathOf(target.file),
11798
- span: spanOf(target.node)
11799
- }))
12207
+ dispatched.targets.map((target) => placeOf(target, displayPathOf))
11800
12208
  );
11801
12209
  if (dispatched.unresolved > 0) {
11802
12210
  stops.add({ callee, reason: "unsettledValue" });
@@ -11828,7 +12236,7 @@ function scanBody(source, ctx, options, read) {
11828
12236
  recordPassedArgs(
11829
12237
  call,
11830
12238
  callee,
11831
- outcome.kind === "followed" ? keyOf(outcome.target) : null
12239
+ outcome.kind === "followed" ? outcome.target : null
11832
12240
  );
11833
12241
  if (outcome.kind === "stopped") {
11834
12242
  const claimed = options.storage !== void 0 && storageClaims(call, callSite.file, options.storage, callSite.method);
@@ -11869,10 +12277,7 @@ function scanBody(source, ctx, options, read) {
11869
12277
  function placeCallee(placements, outcome, call, where) {
11870
12278
  const callee = calleeText(call);
11871
12279
  if (outcome.kind === "followed") {
11872
- placements.place(callee, {
11873
- file: where.displayPathOf(outcome.target.file),
11874
- span: spanOf(outcome.target.node)
11875
- });
12280
+ placements.place(callee, placeOf(outcome.target, where.displayPathOf));
11876
12281
  return;
11877
12282
  }
11878
12283
  if (outcome.matchByName === true) {
@@ -11926,7 +12331,7 @@ function libraryUnit(target, options, binding = functionCallBinding({
11926
12331
  emitEvent: null,
11927
12332
  location: range
11928
12333
  },
11929
- effects,
12334
+ effects: [],
11930
12335
  location: range,
11931
12336
  isDefault: true
11932
12337
  }
@@ -11946,7 +12351,10 @@ function libraryUnit(target, options, binding = functionCallBinding({
11946
12351
  parameters,
11947
12352
  branches: withSlotSources(
11948
12353
  branches.map(
11949
- (branch) => body.extraEffects === void 0 ? branch : { ...branch, extraEffects: body.extraEffects }
12354
+ (branch) => withEffectsThatRun(branch, {
12355
+ effects,
12356
+ extraEffects: body.extraEffects
12357
+ })
11950
12358
  ),
11951
12359
  body.provenance
11952
12360
  ),
@@ -11981,7 +12389,204 @@ function parameterName(param) {
11981
12389
  return named2 !== null && named2.type === "identifier" ? named2.text : null;
11982
12390
  }
11983
12391
 
12392
+ // src/entryPoints.ts
12393
+ function declaredEntryUnits(root, file, options) {
12394
+ const units = [];
12395
+ for (const pack of options.packs) {
12396
+ for (const entry of pack.entryPoints?.(file) ?? []) {
12397
+ const method = entryMethod(root, entry.name, options);
12398
+ if (method === null) {
12399
+ continue;
12400
+ }
12401
+ for (const declared of entry.units) {
12402
+ const raw = libraryUnit(
12403
+ {
12404
+ file,
12405
+ node: method.node,
12406
+ name: declared.name,
12407
+ exportPath: method.exportPath
12408
+ },
12409
+ options,
12410
+ declared.binding
12411
+ );
12412
+ const unit = {
12413
+ ...raw,
12414
+ identity: { ...raw.identity, kind: declared.kind },
12415
+ deployableUnit: declared.deployableUnit,
12416
+ ...declared.metadata !== void 0 ? { packMetadata: declared.metadata } : {}
12417
+ };
12418
+ units.push(unit);
12419
+ options.onReachSeed(unit, {
12420
+ file,
12421
+ node: method.node,
12422
+ enclosingQualifiedName: method.enclosingQualifiedName
12423
+ });
12424
+ }
12425
+ }
12426
+ }
12427
+ return units;
12428
+ }
12429
+ function entryMethod(root, name, options) {
12430
+ const dot = name.lastIndexOf(".");
12431
+ if (dot === -1) {
12432
+ const node = instanceMethodsByName(root, options.bodyBlocks).get(name);
12433
+ return node === void 0 ? null : { node, exportPath: [name], enclosingQualifiedName: null };
12434
+ }
12435
+ const className = name.slice(0, dot);
12436
+ const methodName = name.slice(dot + 1);
12437
+ let found = null;
12438
+ walkDefinitions(root, (info) => {
12439
+ if (found !== null || info.qualifiedName !== className) {
12440
+ return;
12441
+ }
12442
+ const node = info.bodyNode === null ? void 0 : singletonMethodsByName(info.bodyNode, options.bodyBlocks).get(
12443
+ methodName
12444
+ );
12445
+ if (node !== void 0) {
12446
+ found = {
12447
+ node,
12448
+ exportPath: [className, methodName],
12449
+ enclosingQualifiedName: className
12450
+ };
12451
+ }
12452
+ });
12453
+ return found;
12454
+ }
12455
+
12456
+ // src/erbTemplate.ts
12457
+ var OPEN = "<%";
12458
+ var CLOSE = "%>";
12459
+ var LITERAL_OPEN = "<%%";
12460
+ var COMMENT_MARK = "#";
12461
+ var OUTPUT_MARK = "=";
12462
+ var TRIM_OPEN_MARK = "-";
12463
+ var TRIM_CLOSE_MARK = "-";
12464
+ var CODE_CLOSE = "; ";
12465
+ var OUTPUT_CLOSE = "];";
12466
+ var OUTPUT_OPEN = "[";
12467
+ function rubyInErbTemplate(template) {
12468
+ const parts = [];
12469
+ let at = 0;
12470
+ while (at < template.length) {
12471
+ const open = template.indexOf(OPEN, at);
12472
+ if (open === -1) {
12473
+ parts.push(blanked(template.slice(at)));
12474
+ break;
12475
+ }
12476
+ parts.push(blanked(template.slice(at, open)));
12477
+ if (template.startsWith(LITERAL_OPEN, open)) {
12478
+ parts.push(blanked(LITERAL_OPEN));
12479
+ at = open + LITERAL_OPEN.length;
12480
+ continue;
12481
+ }
12482
+ const close = template.indexOf(CLOSE, open + OPEN.length);
12483
+ if (close === -1) {
12484
+ parts.push(blanked(template.slice(open)));
12485
+ break;
12486
+ }
12487
+ parts.push(tagAsRuby(template.slice(open, close + CLOSE.length)));
12488
+ at = close + CLOSE.length;
12489
+ }
12490
+ return parts.join("");
12491
+ }
12492
+ function tagAsRuby(tag) {
12493
+ const mark = tag.charAt(OPEN.length);
12494
+ if (mark === COMMENT_MARK) {
12495
+ return blanked(tag);
12496
+ }
12497
+ const prints = mark === OUTPUT_MARK;
12498
+ const codeStart = OPEN.length + (prints || mark === TRIM_OPEN_MARK ? mark.length : 0);
12499
+ const beforeClose = tag.length - CLOSE.length;
12500
+ const codeEnd = beforeClose > codeStart && tag.charAt(beforeClose - 1) === TRIM_CLOSE_MARK ? beforeClose - 1 : beforeClose;
12501
+ const opening = blanked(tag.slice(0, codeStart));
12502
+ return [
12503
+ prints ? `${OUTPUT_OPEN}${opening.slice(OUTPUT_OPEN.length)}` : opening,
12504
+ tag.slice(codeStart, codeEnd),
12505
+ blanked(tag.slice(codeEnd, beforeClose)),
12506
+ prints ? OUTPUT_CLOSE : CODE_CLOSE
12507
+ ].join("");
12508
+ }
12509
+ function blanked(text3) {
12510
+ return text3.replace(/[^\r\n]/g, " ");
12511
+ }
12512
+
12513
+ // src/facts/callbacks.ts
12514
+ function callbacksIn(patterns) {
12515
+ return patterns.flatMap(
12516
+ (pattern) => pattern.callbacks === void 0 ? [] : [pattern.callbacks]
12517
+ );
12518
+ }
12519
+ function emitClassCallbacks(db, file, classNode, declared) {
12520
+ const body = field(classNode, "body");
12521
+ if (body === null || declared.length === 0) {
12522
+ return;
12523
+ }
12524
+ const classKey = nodeId(file, classNode);
12525
+ for (const statement of runStatements(body)) {
12526
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
12527
+ continue;
12528
+ }
12529
+ const called = field(statement, "method")?.text ?? "";
12530
+ const args = readCallArgs(field(statement, "arguments"));
12531
+ for (const callbacks of declared) {
12532
+ const events = eventsOf(args, called, callbacks, db);
12533
+ for (const event of events) {
12534
+ for (const method of symbolArgumentNames(args, db)) {
12535
+ db.add("classCallback", [classKey, event, method]);
12536
+ }
12537
+ }
12538
+ }
12539
+ }
12540
+ }
12541
+ function eventsOf(args, called, callbacks, db) {
12542
+ const covered = callbacks.registeredBy[called];
12543
+ if (covered === void 0) {
12544
+ return [];
12545
+ }
12546
+ const written = args.keyword[callbacks.eventKeyword];
12547
+ const narrowed = written === void 0 ? null : stringValueOf(written, db);
12548
+ if (narrowed === null) {
12549
+ return covered;
12550
+ }
12551
+ return covered.filter((event) => event === narrowed);
12552
+ }
12553
+
12554
+ // src/facts.ts
12555
+ import path3 from "path";
12556
+ import { SKIP_CHILDREN as SKIP_CHILDREN7, walkDescendants as walkDescendants7 } from "@suss/extractor";
12557
+ function emitRequireFacts(db, filePath, root, known) {
12558
+ walkDescendants7(root, null, children, {
12559
+ at: (node) => {
12560
+ const target = requiredRelativePath(node);
12561
+ if (target === null) {
12562
+ return;
12563
+ }
12564
+ const resolved = path3.resolve(
12565
+ path3.dirname(filePath),
12566
+ target.endsWith(".rb") ? target : `${target}.rb`
12567
+ );
12568
+ if (known.has(resolved)) {
12569
+ db.add("importsFile", [filePath, resolved]);
12570
+ }
12571
+ },
12572
+ into: (node) => node.type === "argument_list" ? SKIP_CHILDREN7 : null
12573
+ });
12574
+ }
12575
+ function requiredRelativePath(node) {
12576
+ if (node.type !== "call" || field(node, "receiver") !== null || field(node, "method")?.text !== "require_relative") {
12577
+ return null;
12578
+ }
12579
+ const { positional } = readCallArgs(field(node, "arguments"));
12580
+ return positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
12581
+ }
12582
+
11984
12583
  // src/moduleSurface.ts
12584
+ import { moduleExportBinding } from "@suss/behavioral-ir";
12585
+ import {
12586
+ MODULE_SURFACE_RECOGNITION,
12587
+ moduleOfFile,
12588
+ settleModules
12589
+ } from "@suss/extractor";
11985
12590
  function settleRubyModules(modules) {
11986
12591
  return settleModules(modules, ["{name}.rb", "../{name}.rb"]);
11987
12592
  }
@@ -12584,12 +13189,12 @@ var SharedGroupIndex = class {
12584
13189
  };
12585
13190
  function emitSharedGroupBinds(db, index, files) {
12586
13191
  for (const at of files) {
12587
- for (const site of sitesIn(index, at)) {
13192
+ for (const site of sitesIn2(index, at)) {
12588
13193
  emitSiteBinds(db, index, site, at.pattern);
12589
13194
  }
12590
13195
  }
12591
13196
  }
12592
- function sitesIn(index, at) {
13197
+ function sitesIn2(index, at) {
12593
13198
  const found = [];
12594
13199
  const visit = (node) => {
12595
13200
  for (const child of children(node)) {
@@ -12830,7 +13435,25 @@ function replaysCleanly(units, discovered) {
12830
13435
  }
12831
13436
  return true;
12832
13437
  }
13438
+ function walkedFilesOf(options) {
13439
+ const given = new Set(options.files);
13440
+ const templates = [
13441
+ ...new Set(options.packs.flatMap((pack) => pack.bootTemplates?.() ?? []))
13442
+ ].filter((file) => !given.has(file)).sort();
13443
+ return {
13444
+ files: [...options.files, ...templates],
13445
+ templates: new Set(templates)
13446
+ };
13447
+ }
13448
+ function rubySourceOf(absPath, templates) {
13449
+ if (!fs3.existsSync(absPath)) {
13450
+ return null;
13451
+ }
13452
+ const text3 = fs3.readFileSync(absPath, "utf8");
13453
+ return templates.has(absPath) ? rubyInErbTemplate(text3) : text3;
13454
+ }
12833
13455
  async function extractRubyProject(options) {
13456
+ const walked = walkedFilesOf(options);
12834
13457
  const timer = options.onTiming !== void 0 ? createTimer() : noopTimer();
12835
13458
  const cacheDir = adapterStamp.declineWhenRunFromSource(
12836
13459
  options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? extractionCacheDirFor(options.projectRoot) : null)
@@ -12847,8 +13470,8 @@ async function extractRubyProject(options) {
12847
13470
  modules: settleRubyModules(options.modules)
12848
13471
  })}`;
12849
13472
  const input = {
12850
- files: cacheDir === null ? [] : options.files,
12851
- adapterPacksDigest: cacheDir === null ? packsDigest : runDigest(packsDigest, options.packs, options.files)
13473
+ files: cacheDir === null ? [] : walked.files,
13474
+ adapterPacksDigest: cacheDir === null ? packsDigest : runDigest(packsDigest, options.packs, walked.files)
12852
13475
  };
12853
13476
  const lookup = await timer.timeAsync(
12854
13477
  "cache.lookup",
@@ -12876,7 +13499,7 @@ async function extractRubyProject(options) {
12876
13499
  const records = plan === null ? null : recordsToReplay(plan.roots, plan.changed, plan.removed);
12877
13500
  if (plan !== null && records !== null) {
12878
13501
  try {
12879
- return await runRuby(options, cache, { plan, records });
13502
+ return await runRuby(options, walked, cache, { plan, records });
12880
13503
  } catch (error) {
12881
13504
  if (!(error instanceof ReplayFailed)) {
12882
13505
  throw error;
@@ -12884,7 +13507,7 @@ async function extractRubyProject(options) {
12884
13507
  }
12885
13508
  }
12886
13509
  options.onCacheDiagnostic?.(lookup.diagnostic);
12887
- return runRuby(options, cache, null);
13510
+ return runRuby(options, walked, cache, null);
12888
13511
  }
12889
13512
  function seedFromStored(seed, nodeOf) {
12890
13513
  const node = nodeOf(seed.key);
@@ -12901,13 +13524,13 @@ function seedFromStored(seed, nodeOf) {
12901
13524
  function addForSeed(bySeed, seedKey, summary) {
12902
13525
  bySeed.set(seedKey, [...bySeed.get(seedKey) ?? [], summary]);
12903
13526
  }
12904
- async function runRuby(options, cache, previous) {
13527
+ async function runRuby(options, walked, cache, previous) {
12905
13528
  const { timer, cacheDir } = cache;
12906
13529
  const db = new Database2();
12907
13530
  const summaries = [];
12908
13531
  const gapHandling = options.gapHandling ?? "permissive";
12909
13532
  const tallies = createPackTallies(options.packs);
12910
- const ledger = cacheDir === null ? null : new DependencyLedger(options.files);
13533
+ const ledger = cacheDir === null ? null : new DependencyLedger(walked.files);
12911
13534
  const log = ledger === null ? null : new FactLog(db, ledger);
12912
13535
  if (ledger !== null) {
12913
13536
  observeDemand(db, ledger);
@@ -12915,12 +13538,12 @@ async function runRuby(options, cache, previous) {
12915
13538
  const parses = options.keptParses ?? keptRubyParses();
12916
13539
  const trees = createFileCache(
12917
13540
  (source, absPath) => parses.parse(absPath, source, parseRubyRoot),
12918
- (absPath) => fs3.existsSync(absPath) ? fs3.readFileSync(absPath, "utf8") : null
13541
+ (absPath) => rubySourceOf(absPath, walked.templates)
12919
13542
  );
12920
13543
  const fileCache = ledger === null ? trees : watchFileCache(trees, ledger);
12921
13544
  const facts = new RunFacts(db, options.packs);
12922
13545
  const { parsed, bodyBlocks, scopeCalls } = facts;
12923
- for (const file of options.files) {
13546
+ for (const file of walked.files) {
12924
13547
  await timer.timeAsync("parse", async () => {
12925
13548
  const root = await trees.get(file);
12926
13549
  if (root !== null) {
@@ -12930,7 +13553,7 @@ async function runRuby(options, cache, previous) {
12930
13553
  }
12931
13554
  });
12932
13555
  }
12933
- options.keptParses?.keepOnly(options.files);
13556
+ options.keptParses?.keepOnly(walked.files);
12934
13557
  const rootsByFile = new Map(parsed.map(({ file, root }) => [file, root]));
12935
13558
  const dynamicNames = timer.time("discover", () => {
12936
13559
  log?.startJoined();
@@ -13056,6 +13679,16 @@ async function runRuby(options, cache, previous) {
13056
13679
  displayPathOf,
13057
13680
  facts: db,
13058
13681
  onReachSeed
13682
+ }),
13683
+ ...declaredEntryUnits(root, file, {
13684
+ packs: options.packs,
13685
+ ...storage === void 0 ? {} : { storage },
13686
+ inheritedMethods,
13687
+ bodyBlocks,
13688
+ dynamicNames: watchedNames,
13689
+ displayPathOf,
13690
+ facts: db,
13691
+ onReachSeed
13059
13692
  })
13060
13693
  ];
13061
13694
  const rawUnits = await timer.timeAsync(
@@ -13104,7 +13737,7 @@ async function runRuby(options, cache, previous) {
13104
13737
  seedKeys.add(moduleKey);
13105
13738
  seeds.push(moduleSeed);
13106
13739
  const readLoadTime = () => ({
13107
- reads: envReadEffects(root, { db, file }),
13740
+ reads: envReadEffects(root, { db, file }).map((one) => one.effect),
13108
13741
  calls: moduleScopeInvocationEffects(
13109
13742
  root,
13110
13743
  inheritedMethods,
@@ -13336,7 +13969,7 @@ async function runRuby(options, cache, previous) {
13336
13969
  buildRubyExtractionReport({
13337
13970
  packs: options.packs,
13338
13971
  tallies,
13339
- filesWalked: options.files.length,
13972
+ filesWalked: walked.files.length,
13340
13973
  summaries: composed
13341
13974
  })
13342
13975
  );
@@ -13562,7 +14195,7 @@ var RubyWhySession = class {
13562
14195
  // src/stubEvidence.ts
13563
14196
  import fs5 from "fs";
13564
14197
  import path6 from "path";
13565
- import { SKIP_CHILDREN as SKIP_CHILDREN7, walkDescendants as walkDescendants7 } from "@suss/extractor";
14198
+ import { SKIP_CHILDREN as SKIP_CHILDREN8, walkDescendants as walkDescendants8 } from "@suss/extractor";
13566
14199
  function bareLetters(name) {
13567
14200
  return name.replaceAll(/[-_]/g, "").toLowerCase();
13568
14201
  }
@@ -13586,14 +14219,14 @@ function requireTarget(node) {
13586
14219
  }
13587
14220
  function requireSitesIn(root, file) {
13588
14221
  const found = [];
13589
- walkDescendants7(root, null, children, {
14222
+ walkDescendants8(root, null, children, {
13590
14223
  at: (node) => {
13591
14224
  const target = requireTarget(node);
13592
14225
  if (target !== null) {
13593
14226
  found.push({ target, file, line: rangeOf(node).start });
13594
14227
  }
13595
14228
  },
13596
- into: (node) => node.type === "argument_list" ? SKIP_CHILDREN7 : null
14229
+ into: (node) => node.type === "argument_list" ? SKIP_CHILDREN8 : null
13597
14230
  });
13598
14231
  return found;
13599
14232
  }