@suss/adapter-python 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
@@ -989,6 +989,8 @@ import {
989
989
  addTemplateRows,
990
990
  NAMED_STORE_NAME,
991
991
  RECEIVER_STORE_NAME,
992
+ SELF_AS_CLASS_NAME,
993
+ SELF_AS_INSTANCE_NAME,
992
994
  SPREAD_ENTRY,
993
995
  startsAtName,
994
996
  UNPLACED_STORE_NAME,
@@ -1005,6 +1007,8 @@ var CONVERTING_BUILTINS = /* @__PURE__ */ new Set([
1005
1007
  ]);
1006
1008
  var ATTRIBUTE_GETTER = "getattr";
1007
1009
  var POSITION_COUNTER = "enumerate";
1010
+ var CLASS_METHOD_DECORATOR = "classmethod";
1011
+ var STATIC_METHOD_DECORATOR = "staticmethod";
1008
1012
 
1009
1013
  // src/facts/values.ts
1010
1014
  function nodeId(filePath, node) {
@@ -1684,6 +1688,26 @@ function yieldedValue(node) {
1684
1688
  }
1685
1689
  return children(node).find((child) => child?.type !== "comment") ?? null;
1686
1690
  }
1691
+ function emitReceiver(emitter, fn, paramKey, classKey) {
1692
+ const form = receiverFormOf(fn);
1693
+ if (form === SELF_AS_CLASS_NAME) {
1694
+ add(emitter, "binds", paramKey, classKey);
1695
+ } else {
1696
+ add(emitter, "instanceOf", paramKey, classKey);
1697
+ }
1698
+ if (form !== null) {
1699
+ add(emitter, "selfReceiver", nodeId(emitter.filePath, fn), paramKey, form);
1700
+ }
1701
+ }
1702
+ function receiverFormOf(fn) {
1703
+ const decorators = fn.parent?.type === "decorated_definition" ? stripDecorators(fn.parent).decorators.map(
1704
+ (decorator) => decorator.text.slice(1).trim()
1705
+ ) : [];
1706
+ if (decorators.includes(STATIC_METHOD_DECORATOR)) {
1707
+ return null;
1708
+ }
1709
+ return decorators.includes(CLASS_METHOD_DECORATOR) ? SELF_AS_CLASS_NAME : SELF_AS_INSTANCE_NAME;
1710
+ }
1687
1711
  function emitFunctionFacts(emitter, fn, classKey) {
1688
1712
  const funcKey = nodeId(emitter.filePath, fn);
1689
1713
  add(emitter, "func", funcKey);
@@ -1707,7 +1731,7 @@ function emitFunctionFacts(emitter, fn, classKey) {
1707
1731
  }
1708
1732
  if (classKey !== void 0 && position === -1) {
1709
1733
  receiver = { classKey, name: paramName.text };
1710
- add(emitter, "instanceOf", paramKey, classKey);
1734
+ emitReceiver(emitter, fn, paramKey, classKey);
1711
1735
  }
1712
1736
  add(emitter, "paramNamed", funcKey, paramName.text, paramKey);
1713
1737
  const fallback2 = field(param, "value");
@@ -3796,6 +3820,7 @@ import {
3796
3820
  absentReading,
3797
3821
  ambiguousReading,
3798
3822
  andThenReading,
3823
+ effectsFiringOn as effectsFiringOn2,
3799
3824
  firstWrittenReading,
3800
3825
  mapReading,
3801
3826
  unreadableReading,
@@ -3816,7 +3841,7 @@ import { redirectDeliveryWhenSet } from "@suss/extractor";
3816
3841
  import { hostOf, pathOf, scalarOf } from "@suss/values";
3817
3842
 
3818
3843
  // src/paths/bodyBranches.ts
3819
- import { enumerateOrDegrade as enumerateOrDegrade2, guardsHoldOn } from "@suss/extractor";
3844
+ import { effectsFiringOn, enumerateOrDegrade } from "@suss/extractor";
3820
3845
 
3821
3846
  // src/provenance.ts
3822
3847
  import { sourceRefsOf } from "@suss/extractor";
@@ -3953,7 +3978,7 @@ import { storageBinding as storageBinding2 } from "@suss/ir-core";
3953
3978
  import { answersFor } from "@suss/resolution";
3954
3979
 
3955
3980
  // src/paths/effects.ts
3956
- import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
3981
+ import { sharedGatingConditions, statementSites } from "@suss/extractor";
3957
3982
  import { constantOf as constantOf2, literalOf as literalOf4 } from "@suss/values";
3958
3983
 
3959
3984
  // src/paths/lowering.ts
@@ -4067,6 +4092,7 @@ var Lowerer = class {
4067
4092
  ) ?? null;
4068
4093
  return {
4069
4094
  kind: "try",
4095
+ lines: rangeOf(node),
4070
4096
  tryBody: this.lowerBlock(field(node, "body")),
4071
4097
  catchBody: excepts.length > 0 ? excepts.flatMap((clause) => this.lowerBlock(childBlock(clause))) : null,
4072
4098
  finallyBody: finallyClause === null ? null : this.lowerBlock(childBlock(finallyClause)),
@@ -4107,12 +4133,19 @@ var Lowerer = class {
4107
4133
  }
4108
4134
  if (node.type === "while_statement" || node.type === "for_statement") {
4109
4135
  const header = node.type === "while_statement" ? field(node, "condition") : field(node, "right");
4136
+ const body = field(node, "body");
4110
4137
  return this.attach(
4111
4138
  {
4112
4139
  kind: "loop",
4113
4140
  condition: handleOf(header),
4114
- body: this.lowerBlock(field(node, "body")),
4115
- exitKind: exitKindOf(node, this.thrown)
4141
+ body: this.lowerBlock(body),
4142
+ exitKind: exitKindOf(node, this.thrown),
4143
+ // An `else` clause runs once the loop is done, so it is no part
4144
+ // of what repeats.
4145
+ lines: {
4146
+ start: rangeOf(node).start,
4147
+ end: rangeOf(body ?? node).end
4148
+ }
4116
4149
  },
4117
4150
  [header]
4118
4151
  );
@@ -4144,6 +4177,9 @@ function lowerPythonBody(body, terminals, thrown = new NodeSet()) {
4144
4177
  // src/paths/effects.ts
4145
4178
  var NESTED_DEFINITION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
4146
4179
  function literalArgOf(node, facts) {
4180
+ if (node.type === "none") {
4181
+ return { kind: "null" };
4182
+ }
4147
4183
  const value = evaluatedValue(node, facts);
4148
4184
  const text2 = literalOf4(value);
4149
4185
  if (text2 !== null) {
@@ -4221,9 +4257,6 @@ function argsOf(call, facts) {
4221
4257
  function calleeText(call) {
4222
4258
  return field(call, "function")?.text ?? call.text;
4223
4259
  }
4224
- function invocationEffects(definitionNode, facts) {
4225
- return invocationSites(definitionNode, facts).map((site) => site.effect);
4226
- }
4227
4260
  function invocationSites(definitionNode, facts) {
4228
4261
  const body = field(definitionNode, "body");
4229
4262
  if (body === null) {
@@ -4279,33 +4312,26 @@ function gatingInputs(body, gatingOf, facts) {
4279
4312
  );
4280
4313
  return operands.length === 0 ? void 0 : guardInputs(operands, { facts, unit });
4281
4314
  }
4315
+ function sitesIn(body, nodes) {
4316
+ return statementSites(nodes, {
4317
+ statementOf: (node) => enclosingStatement(node, body),
4318
+ lower: (statements) => lowerPythonBody(body, statements),
4319
+ firstLine: (node) => rangeOf(node).start
4320
+ });
4321
+ }
4322
+ function effectSitesIn(body, nodes) {
4323
+ if (body === null || nodes.length === 0) {
4324
+ return (node) => ({ line: rangeOf(node).start });
4325
+ }
4326
+ return sitesIn(body, nodes).plainSiteOf;
4327
+ }
4282
4328
  function invocationEffectsIn(body, written, facts) {
4283
4329
  const calls = [...written].sort((a, b) => a.endIndex - b.endIndex);
4284
4330
  if (calls.length === 0) {
4285
4331
  return [];
4286
4332
  }
4287
- const statementOf3 = /* @__PURE__ */ new Map();
4288
- const statements = [];
4289
- for (const call of calls) {
4290
- const statement = enclosingStatement(call, body);
4291
- if (statement === null) {
4292
- continue;
4293
- }
4294
- statementOf3.set(call.id, statement);
4295
- statements.push(statement);
4296
- }
4297
- const lowered = lowerPythonBody(body, statements);
4298
- const enumerated = enumerateOrDegrade(
4299
- {
4300
- statements: lowered.statements,
4301
- terminalsByStmt: lowered.terminalsByStmt
4302
- },
4303
- statements
4304
- );
4305
- const gatingOf = calls.map((call) => {
4306
- const statement = statementOf3.get(call.id);
4307
- return statement === void 0 ? void 0 : enumerated.byTerminal.get(statement);
4308
- });
4333
+ const sites = sitesIn(body, calls);
4334
+ const gatingOf = calls.map((call) => sites.pathsTo(call));
4309
4335
  askWrittenValues(
4310
4336
  calls.flatMap((call, index) => effectValueNodes(call, gatingOf[index])),
4311
4337
  facts
@@ -4323,8 +4349,10 @@ function invocationEffectsIn(body, written, facts) {
4323
4349
  callee: calleeText(call),
4324
4350
  args: argsOf(call, facts),
4325
4351
  async: false,
4326
- ...conditions.length > 0 ? { preconditions: conditions } : {}
4327
- }
4352
+ ...conditions.length > 0 ? { preconditions: conditions } : {},
4353
+ location: rangeOf(call)
4354
+ },
4355
+ site: sites.siteOf(call, conditions)
4328
4356
  };
4329
4357
  });
4330
4358
  }
@@ -4429,7 +4457,7 @@ function rawSqlEffects(calls, options) {
4429
4457
  for (const match of matchesIn(calls, options)) {
4430
4458
  for (const access of match.accesses) {
4431
4459
  const effect = effectFor(match, access);
4432
- found.effects.push(effect);
4460
+ found.effects.push({ effect, at: match.call });
4433
4461
  for (const slot of boundSlots(access, match.binds)) {
4434
4462
  found.slots.push({ ...slot, effect });
4435
4463
  }
@@ -4536,7 +4564,8 @@ function effectFor(match, access) {
4536
4564
  fields: access.fields,
4537
4565
  ...access.selector.length > 0 ? { selector: access.selector } : {},
4538
4566
  operation
4539
- }
4567
+ },
4568
+ location: rangeOf(match.call)
4540
4569
  };
4541
4570
  }
4542
4571
  var NO_GROUP = "default";
@@ -5050,7 +5079,8 @@ function effectFor2(pattern, chain, operation, options) {
5050
5079
  fields: fieldsOf(chain, valueMethods),
5051
5080
  operation,
5052
5081
  ...picked.length > 0 ? { selector: picked } : {}
5053
- }
5082
+ },
5083
+ location: rangeOf(chain.last)
5054
5084
  };
5055
5085
  if (options.statesSlots !== void 0) {
5056
5086
  const slots = slotsOf(chain, valueMethods);
@@ -5147,7 +5177,12 @@ function storageCallIds(calls, options) {
5147
5177
  }
5148
5178
  function storageEffects(calls, options) {
5149
5179
  return matchedChains(calls, options).flatMap(
5150
- ({ chain, pattern, operation }) => operation === null ? [] : [effectFor2(pattern, chain, operation, options)]
5180
+ ({ chain, pattern, operation }) => operation === null ? [] : [
5181
+ {
5182
+ effect: effectFor2(pattern, chain, operation, options),
5183
+ at: chain.last
5184
+ }
5185
+ ]
5151
5186
  );
5152
5187
  }
5153
5188
  var storageByScope = /* @__PURE__ */ new WeakMap();
@@ -5345,11 +5380,6 @@ function bodyTerminals(body, raised, continuations = []) {
5345
5380
  ];
5346
5381
  return found.sort((a, b) => a.statement.startIndex - b.statement.startIndex);
5347
5382
  }
5348
- function effectsReaching(effects, conditions) {
5349
- return effects.filter(
5350
- (effect) => guardsHoldOn(effect.preconditions, conditions)
5351
- );
5352
- }
5353
5383
  function conditionInputs(pathSets, options) {
5354
5384
  const unit = options.body?.parent ?? null;
5355
5385
  if (options.facts === void 0 || unit === null) {
@@ -5371,7 +5401,7 @@ function enumerateBodyBranches(options) {
5371
5401
  ...options.terminals.filter((found) => found.type === "continuation").map((found) => found.statement)
5372
5402
  ]);
5373
5403
  const lowered = lowerPythonBody(options.body, statements, leavesByCall);
5374
- const enumerated = enumerateOrDegrade2(
5404
+ const enumerated = enumerateOrDegrade(
5375
5405
  {
5376
5406
  statements: lowered.statements,
5377
5407
  terminalsByStmt: lowered.terminalsByStmt,
@@ -5384,7 +5414,8 @@ function enumerateBodyBranches(options) {
5384
5414
  options
5385
5415
  );
5386
5416
  const branches = [];
5387
- const push = (branch, paths) => {
5417
+ const push = (branch, paths, placement) => {
5418
+ const loopAround = placement?.loopAround;
5388
5419
  for (const path8 of paths) {
5389
5420
  const conditions = path8.map((condition) => ({
5390
5421
  sourceText: condition.sourceText,
@@ -5392,10 +5423,14 @@ function enumerateBodyBranches(options) {
5392
5423
  polarity: condition.polarity,
5393
5424
  source: condition.source
5394
5425
  }));
5395
- branches.push({
5426
+ const ending = {
5396
5427
  ...branch,
5397
5428
  conditions,
5398
- effects: effectsReaching(options.effects, conditions),
5429
+ ...loopAround === void 0 ? {} : { loopAround }
5430
+ };
5431
+ branches.push({
5432
+ ...ending,
5433
+ effects: effectsFiringOn(options.effects, ending),
5399
5434
  isDefault: path8.length === 0
5400
5435
  });
5401
5436
  }
@@ -5403,11 +5438,15 @@ function enumerateBodyBranches(options) {
5403
5438
  for (const found of options.terminals) {
5404
5439
  const paths = enumerated.byTerminal.get(found.statement);
5405
5440
  if (paths !== void 0) {
5406
- push(options.branchOf(found), paths);
5441
+ push(
5442
+ options.branchOf(found),
5443
+ paths,
5444
+ enumerated.placement.get(found.statement)
5445
+ );
5407
5446
  }
5408
5447
  }
5409
5448
  if (options.fallthrough !== void 0) {
5410
- push(options.fallthrough, enumerated.fallthrough);
5449
+ push(options.fallthrough, enumerated.fallthrough, void 0);
5411
5450
  }
5412
5451
  return branches;
5413
5452
  }
@@ -5582,15 +5621,10 @@ function writtenMessageOf(call) {
5582
5621
  import { helperPathsOnVariables, splitAtHandOffs } from "@suss/extractor";
5583
5622
  function throughResponseHelpers(branches, sites, options) {
5584
5623
  const handOffs = [];
5585
- for (const { call, effect } of sites) {
5624
+ for (const { call, site } of sites) {
5586
5625
  const helper = helperBranchesAt(call, options);
5587
5626
  if (helper !== null) {
5588
- handOffs.push({
5589
- helper,
5590
- preconditions: effect.preconditions,
5591
- line: rangeOf(call).start,
5592
- at: rangeOf(call)
5593
- });
5627
+ handOffs.push({ ...site, helper, at: rangeOf(call) });
5594
5628
  }
5595
5629
  }
5596
5630
  return handOffs.length === 0 ? [...branches] : branches.flatMap((branch) => splitAtHandOffs(branch, handOffs));
@@ -5837,7 +5871,7 @@ function callerBranches(definition, reading) {
5837
5871
  body,
5838
5872
  terminals: bodyTerminals(body, raised),
5839
5873
  raised,
5840
- effects,
5874
+ effects: sites,
5841
5875
  branchOf: (found2) => found2.type === "raise" ? { terminal: found2.terminal, location: rangeOf(found2.statement) } : handsBackAt(rangeOf(found2.statement)),
5842
5876
  fallthrough: handsBackAt(range),
5843
5877
  facts: reading.facts,
@@ -6417,9 +6451,7 @@ function envReadEffects(root, module, facts) {
6417
6451
  const syntax = envReadSyntaxAt(node, scope);
6418
6452
  if (syntax !== null) {
6419
6453
  const name = stringLiteralValue(syntax.name);
6420
- if (name !== null) {
6421
- slots.push({ read: { name, defaulted: syntax.defaulted } });
6422
- }
6454
+ slots.push({ read: { name, defaulted: syntax.defaulted }, at: node });
6423
6455
  return;
6424
6456
  }
6425
6457
  if (readsHelpers && node.type === "call") {
@@ -6439,21 +6471,25 @@ function envReadEffects(root, module, facts) {
6439
6471
  );
6440
6472
  return slots.flatMap((slot) => {
6441
6473
  if ("read" in slot) {
6442
- return [configReadEffect(slot.read)];
6474
+ return [{ effect: configReadEffect(slot.read, slot.at), at: slot.at }];
6443
6475
  }
6444
- return throughHelpers.get(slot.call.id) ?? [];
6476
+ return (throughHelpers.get(slot.call.id) ?? []).map((effect) => ({
6477
+ effect,
6478
+ at: slot.call
6479
+ }));
6445
6480
  });
6446
6481
  }
6447
- function configReadEffect(read) {
6482
+ function configReadEffect(read, at) {
6448
6483
  return {
6449
6484
  type: "interaction",
6450
6485
  binding: runtimeConfigBinding({ recognition: PYTHON_ENV_RECOGNITION }),
6451
- callee: `os.environ["${read.name}"]`,
6486
+ callee: read.name === null ? at.text : `os.environ["${read.name}"]`,
6452
6487
  interaction: {
6453
6488
  class: "config-read",
6454
6489
  name: read.name,
6455
6490
  defaulted: read.defaulted
6456
- }
6491
+ },
6492
+ location: rangeOf(at)
6457
6493
  };
6458
6494
  }
6459
6495
  function envReadSyntaxAt(node, scope) {
@@ -6659,6 +6695,15 @@ function argumentsByParameter(db, call, calleeKey2) {
6659
6695
  }
6660
6696
  return argumentAt2;
6661
6697
  }
6698
+ function handsOnParameter(argument) {
6699
+ const caller = enclosingFunction(argument);
6700
+ if (argument.type !== "identifier" || caller === null) {
6701
+ return false;
6702
+ }
6703
+ return children(field(caller, "parameters")).some(
6704
+ (param) => parameterIdentifier(param)?.text === argument.text
6705
+ );
6706
+ }
6662
6707
  function readsThroughHelper(db, call, argumentAt2, siteById, named) {
6663
6708
  const reads = /* @__PURE__ */ new Map();
6664
6709
  for (const [parameter, argument] of argumentAt2) {
@@ -6668,7 +6713,7 @@ function readsThroughHelper(db, call, argumentAt2, siteById, named) {
6668
6713
  `${NAMED_PARAMETER}${parameter}`,
6669
6714
  describeSites(sites, siteById)
6670
6715
  );
6671
- if (sites === void 0) {
6716
+ if (sites === void 0 || handsOnParameter(argument)) {
6672
6717
  continue;
6673
6718
  }
6674
6719
  const name = stringLiteralValue(argument) ?? stringValueOf(argument, db);
@@ -6685,7 +6730,7 @@ function readsThroughHelper(db, call, argumentAt2, siteById, named) {
6685
6730
  }
6686
6731
  const defaultedAtCall = isDefaultedAt(call);
6687
6732
  return [...reads].map(
6688
- ([name, defaulted]) => configReadEffect({ name, defaulted: defaulted || defaultedAtCall })
6733
+ ([name, defaulted]) => configReadEffect({ name, defaulted: defaulted || defaultedAtCall }, call)
6689
6734
  );
6690
6735
  }
6691
6736
 
@@ -6762,7 +6807,12 @@ function returnedBodyShape(statement) {
6762
6807
 
6763
6808
  // src/streamWrites.ts
6764
6809
  import { ioBinding } from "@suss/behavioral-ir";
6765
- import { SKIP_CHILDREN as SKIP_CHILDREN2, walkDescendants as walkDescendants2 } from "@suss/extractor";
6810
+ import {
6811
+ callSiteKey,
6812
+ numberRepeatedCallSites,
6813
+ SKIP_CHILDREN as SKIP_CHILDREN2,
6814
+ walkDescendants as walkDescendants2
6815
+ } from "@suss/extractor";
6766
6816
 
6767
6817
  // src/stdlibNames.ts
6768
6818
  function isStdlibModule(node, scope, module) {
@@ -6792,6 +6842,11 @@ var PYTHON_STREAM_RECOGNITION = "python-stdlib";
6792
6842
  var STREAMS = /* @__PURE__ */ new Set(["stdout", "stderr"]);
6793
6843
  var DEFERRED_BODY_TYPES2 = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
6794
6844
  function streamWriteEffects(root, module) {
6845
+ return numberRepeatedCallSites(
6846
+ writesUnder(root, module).map((write) => [write])
6847
+ ).flat();
6848
+ }
6849
+ function writesUnder(root, module) {
6795
6850
  const found = [];
6796
6851
  const startScope = module.scopeFor.get(root.id) ?? module.moduleScope;
6797
6852
  walkDescendants2(root, startScope, children, {
@@ -6801,7 +6856,7 @@ function streamWriteEffects(root, module) {
6801
6856
  }
6802
6857
  const write = streamWriteAt(node, scope);
6803
6858
  if (write !== null) {
6804
- found.push(write);
6859
+ found.push({ effect: write, at: node });
6805
6860
  }
6806
6861
  },
6807
6862
  into: (node, scope) => {
@@ -6840,16 +6895,17 @@ function standardStream(node, scope) {
6840
6895
  }
6841
6896
  function writeEffect(site) {
6842
6897
  const { call, callee, target } = site;
6843
- return {
6898
+ const effect = {
6844
6899
  type: "interaction",
6845
6900
  binding: ioBinding({ recognition: PYTHON_STREAM_RECOGNITION, target }),
6846
6901
  callee,
6847
- groupId: `${call.startPosition.row + 1}:${call.startPosition.column + 1}`,
6848
6902
  interaction: {
6849
6903
  class: "stream-write",
6850
6904
  ...payloadOf(site.positional, site.scope)
6851
- }
6905
+ },
6906
+ location: rangeOf(call)
6852
6907
  };
6908
+ return { ...effect, groupId: callSiteKey(effect) };
6853
6909
  }
6854
6910
  function payloadOf(args, scope) {
6855
6911
  const only = args[0];
@@ -7710,7 +7766,8 @@ function buildRouteUnit(options) {
7710
7766
  options.responseShape,
7711
7767
  () => readReturnAnnotation(definitionNode, enclosingScope, ctx)
7712
7768
  );
7713
- const effects = invocationEffects(definitionNode, options.facts);
7769
+ const calls = invocationSites(definitionNode, options.facts);
7770
+ const effects = calls.map((call) => call.effect);
7714
7771
  const extra = recognizedBodyEffects(
7715
7772
  definitionNode,
7716
7773
  module,
@@ -7719,7 +7776,7 @@ function buildRouteUnit(options) {
7719
7776
  );
7720
7777
  const perTerminal = branchesPerTerminal({
7721
7778
  definitionNode,
7722
- effects,
7779
+ effects: calls,
7723
7780
  responseStatusCalls: options.responseStatusCalls,
7724
7781
  ctx: {
7725
7782
  pattern,
@@ -7862,12 +7919,17 @@ function unreadRoleReadings(parameters, range) {
7862
7919
  }
7863
7920
  function recognizedBodyEffects(definitionNode, module, storageLookup, facts) {
7864
7921
  const storage = storageLookup === void 0 ? { effects: [], slots: [] } : bodyStorage(definitionNode, storageLookup);
7922
+ const found = [
7923
+ ...envReadEffects(definitionNode, module, facts),
7924
+ ...storage.effects,
7925
+ ...streamWriteEffects(definitionNode, module)
7926
+ ];
7927
+ const siteOf2 = effectSitesIn(
7928
+ field(definitionNode, "body"),
7929
+ found.map((one) => one.at)
7930
+ );
7865
7931
  return {
7866
- effects: [
7867
- ...envReadEffects(definitionNode, module, facts),
7868
- ...storage.effects,
7869
- ...streamWriteEffects(definitionNode, module)
7870
- ],
7932
+ effects: found.map(({ effect, at }) => ({ effect, site: siteOf2(at) })),
7871
7933
  provenance: storage.slots.length === 0 || storageLookup === void 0 ? [] : slotProvenance(storage.slots, {
7872
7934
  facts: storageLookup.facts,
7873
7935
  unit: definitionNode
@@ -7875,12 +7937,13 @@ function recognizedBodyEffects(definitionNode, module, storageLookup, facts) {
7875
7937
  };
7876
7938
  }
7877
7939
  function withBodyEffects(branch, extra) {
7878
- if (extra.effects.length === 0) {
7940
+ const effects = effectsFiringOn2(extra.effects, branch);
7941
+ if (effects.length === 0) {
7879
7942
  return branch;
7880
7943
  }
7881
7944
  return {
7882
7945
  ...branch,
7883
- extraEffects: extra.effects,
7946
+ extraEffects: effects,
7884
7947
  ...extra.provenance.length === 0 ? {} : { provenance: extra.provenance }
7885
7948
  };
7886
7949
  }
@@ -8315,6 +8378,33 @@ function buildPythonExtractionReport(args) {
8315
8378
  });
8316
8379
  }
8317
8380
 
8381
+ // src/reach/closure.ts
8382
+ import {
8383
+ functionCallBinding,
8384
+ placeArgTargets,
8385
+ placeCalleeParameters,
8386
+ placeCalls,
8387
+ placePossibleCalls,
8388
+ recordParameterGaps,
8389
+ StopList,
8390
+ sameDeclarations,
8391
+ TargetPlacements,
8392
+ unfollowedCallGap,
8393
+ worthRecording
8394
+ } from "@suss/behavioral-ir";
8395
+ import { Database, evaluate, lit, rule, variable as v } from "@suss/datalog";
8396
+ import {
8397
+ assembleSummary,
8398
+ SKIP_CHILDREN as SKIP_CHILDREN5,
8399
+ walkDescendants as walkDescendants6
8400
+ } from "@suss/extractor";
8401
+ import {
8402
+ keptVariants,
8403
+ noDependencies,
8404
+ plainKeyOf,
8405
+ variantKey
8406
+ } from "@suss/resolution";
8407
+
8318
8408
  // src/exits.ts
8319
8409
  import {
8320
8410
  markReturnsAsExitCode,
@@ -8451,96 +8541,6 @@ function returnTerminal(shape, location) {
8451
8541
  };
8452
8542
  }
8453
8543
 
8454
- // src/importedDefinitions.ts
8455
- import { isObserved, noteKeyRead as noteKeyRead2 } from "@suss/resolution";
8456
- function importedDefinitionLookup(db, files) {
8457
- const fileOfModuleScope = /* @__PURE__ */ new Map();
8458
- const byFile = /* @__PURE__ */ new Map();
8459
- for (const file of files) {
8460
- fileOfModuleScope.set(file.module.moduleScope.node.id, file.file);
8461
- byFile.set(file.file, file);
8462
- }
8463
- const found = /* @__PURE__ */ new Map();
8464
- return (scope, name) => {
8465
- const fromFile = fileOfModuleScope.get(moduleScopeOf(scope).node.id);
8466
- if (fromFile === void 0) {
8467
- return null;
8468
- }
8469
- const key = `${fromFile}#${name}`;
8470
- let definition = found.get(key);
8471
- if (definition === void 0) {
8472
- definition = definitionAmong(originsOf(db, key), byFile);
8473
- found.set(key, definition);
8474
- } else if (isObserved(db)) {
8475
- originsOf(db, key);
8476
- }
8477
- if (definition !== null) {
8478
- noteKeyRead2(db, definition.file);
8479
- }
8480
- return definition;
8481
- };
8482
- }
8483
- function definitionAmong(origins, byFile) {
8484
- for (const origin of origins) {
8485
- const file = byFile.get(origin.module);
8486
- const binding = file?.module.moduleScope.bindings.get(origin.name);
8487
- if (file === void 0 || binding === void 0) {
8488
- continue;
8489
- }
8490
- const node = definedNodeOf(binding);
8491
- if (node !== null) {
8492
- return {
8493
- node,
8494
- file: file.file,
8495
- moduleScope: file.module.moduleScope,
8496
- scopeFor: file.module.scopeFor
8497
- };
8498
- }
8499
- }
8500
- return null;
8501
- }
8502
- function definedNodeOf(binding) {
8503
- if (binding.kind === "classDef") {
8504
- return binding.node;
8505
- }
8506
- if (binding.kind === "assignment") {
8507
- return binding.value;
8508
- }
8509
- return null;
8510
- }
8511
-
8512
- // src/moduleSurface.ts
8513
- import { moduleExportBinding } from "@suss/behavioral-ir";
8514
- import {
8515
- MODULE_SURFACE_RECOGNITION,
8516
- moduleOfFile,
8517
- settleModules
8518
- } from "@suss/extractor";
8519
- import { noteKeyRead as noteKeyRead4 } from "@suss/resolution";
8520
-
8521
- // src/reach/closure.ts
8522
- import {
8523
- functionCallBinding,
8524
- placeArgTargets,
8525
- placeCalleeParameters,
8526
- placeCalls,
8527
- placePossibleCalls,
8528
- recordParameterGaps,
8529
- StopList,
8530
- TargetPlacements,
8531
- unfollowedCallGap,
8532
- worthRecording
8533
- } from "@suss/behavioral-ir";
8534
- import { Database, evaluate, lit, rule, variable as v } from "@suss/datalog";
8535
- import {
8536
- assembleSummary,
8537
- SKIP_CHILDREN as SKIP_CHILDREN5,
8538
- walkDescendants as walkDescendants6
8539
- } from "@suss/extractor";
8540
- import {
8541
- noDependencies
8542
- } from "@suss/resolution";
8543
-
8544
8544
  // src/flags.ts
8545
8545
  import { SKIP_CHILDREN as SKIP_CHILDREN4, walkDescendants as walkDescendants5 } from "@suss/extractor";
8546
8546
  var PARSE_METHODS = /* @__PURE__ */ new Set(["parse_args", "parse_known_args"]);
@@ -8623,11 +8623,19 @@ function methodCalledOn(call) {
8623
8623
 
8624
8624
  // src/reach/resolveCallee.ts
8625
8625
  import {
8626
+ answersFor as answersFor2,
8626
8627
  calleeOutcomeOf,
8627
8628
  calleeOutcomes,
8628
8629
  dispatchOutcomes,
8629
- noteKeyRead as noteKeyRead3,
8630
+ fileOfKey,
8631
+ memberOf,
8632
+ nodeOfKey as nodeOfKey2,
8633
+ noteKeyRead as noteKeyRead2,
8630
8634
  RECEIVER_STORE_NAME as RECEIVER_STORE_NAME2,
8635
+ resolutionProgram as resolutionProgram2,
8636
+ SELF_AS_CLASS_NAME as SELF_AS_CLASS_NAME2,
8637
+ selfFormOf,
8638
+ selfResultOf,
8631
8639
  writtenSourcesOf
8632
8640
  } from "@suss/resolution";
8633
8641
  var STOP_FOR = {
@@ -8643,14 +8651,21 @@ var NO_DECLARATION = {
8643
8651
  function calleeSpellings(calls, ctx) {
8644
8652
  const spellingOf = /* @__PURE__ */ new Map();
8645
8653
  const keys = /* @__PURE__ */ new Set();
8654
+ const receivers = /* @__PURE__ */ new Set();
8646
8655
  for (const { call, site } of calls) {
8647
8656
  const spelling = spellingFor2(call, site, ctx);
8648
8657
  spellingOf.set(call.id, spelling);
8649
8658
  if (spelling.kind === "key") {
8650
8659
  keys.add(spelling.key);
8651
8660
  }
8661
+ const receiver = receiverOf(call, site);
8662
+ if (receiver !== null) {
8663
+ receivers.add(receiver.key);
8664
+ }
8652
8665
  }
8653
- return { spellingOf, outcomes: calleeOutcomes(ctx.facts, [...keys]) };
8666
+ const outcomes = calleeOutcomes(ctx.facts, [...keys]);
8667
+ resolveCalls(ctx.facts, [...receivers]);
8668
+ return { spellingOf, outcomes };
8654
8669
  }
8655
8670
  function resolveCallee2(call, site, ctx, read) {
8656
8671
  const spelling = read?.spellingOf.get(call.id) ?? spellingFor2(call, site, ctx);
@@ -8658,10 +8673,100 @@ function resolveCallee2(call, site, ctx, read) {
8658
8673
  return spelling;
8659
8674
  }
8660
8675
  const outcome = read?.outcomes.get(spelling.key) ?? calleeOutcomeOf(ctx.facts, spelling.key);
8661
- if (selfCallInMixin(call, outcome, site, ctx)) {
8662
- return { kind: "stopped", reason: "selfCallInMixin" };
8676
+ const settled = selfCallInMixin(call, outcome, site, ctx) ? { kind: "stopped", reason: "selfCallInMixin" } : asCallee(outcome, site.owner, ctx);
8677
+ return onReceiverClass(call, site, ctx, settled) ?? settled;
8678
+ }
8679
+ function receiverOf(call, site) {
8680
+ const callee = readThrough(field(call, "function"));
8681
+ const object = callee?.type === "attribute" ? field(callee, "object") : null;
8682
+ const name = callee === null ? void 0 : field(callee, "attribute")?.text;
8683
+ if (object === null || name === void 0) {
8684
+ return null;
8685
+ }
8686
+ return {
8687
+ key: readKey(site.file.file, object, enclosingFunction(object)),
8688
+ name
8689
+ };
8690
+ }
8691
+ function onReceiverClass(call, site, ctx, settled) {
8692
+ const own = ownReceiverOf(site, ctx);
8693
+ const callee = readThrough(field(call, "function"));
8694
+ if (callee?.type === "identifier") {
8695
+ const calleeKey2 = readKey(
8696
+ site.file.file,
8697
+ callee,
8698
+ enclosingFunction(callee)
8699
+ );
8700
+ return own !== null && selfFormOf(ctx.facts, calleeKey2, own.key) === SELF_AS_CLASS_NAME2 ? methodOn(own.cls, CONSTRUCTOR_METHOD, ctx) : null;
8701
+ }
8702
+ const read = receiverOf(call, site);
8703
+ if (read === null) {
8704
+ return null;
8705
+ }
8706
+ if (own !== null && selfFormOf(ctx.facts, read.key, own.key) !== null) {
8707
+ return methodOn(own.cls, read.name, ctx);
8708
+ }
8709
+ const sentTo = classReturnedTo(read.key, own, ctx);
8710
+ if (sentTo !== null) {
8711
+ return methodOn(sentTo, read.name, ctx);
8712
+ }
8713
+ const receiverClass = settled.kind === "followed" ? onlyClassOf(read.key, ctx) : null;
8714
+ return receiverClass === null || settled.kind !== "followed" ? null : { kind: "followed", target: runningOn(settled.target, receiverClass) };
8715
+ }
8716
+ function ownReceiverOf(site, ctx) {
8717
+ const [row] = ctx.facts.lookup("selfReceiver", 0, site.owner);
8718
+ const cls = site.receiverClass ?? classOfFunction(site.owner, ctx);
8719
+ return row === void 0 || cls === null ? null : { key: String(row[1]), cls };
8720
+ }
8721
+ function classReturnedTo(key, own, ctx) {
8722
+ const returned = selfResultOf(ctx.facts, key);
8723
+ if (returned === null) {
8724
+ return null;
8725
+ }
8726
+ if (own !== null && returned.receiver === own.key) {
8727
+ return own.cls;
8663
8728
  }
8664
- return asCallee(outcome, site.owner, ctx);
8729
+ resolveCalls(ctx.facts, [returned.receiver]);
8730
+ return onlyClassOf(returned.receiver, ctx);
8731
+ }
8732
+ var CONSTRUCTOR_METHOD = "__init__";
8733
+ function methodOn(cls, name, ctx) {
8734
+ const key = memberOf(ctx.facts, cls.key, name, resolutionProgram2());
8735
+ const found = key === null ? null : functionCallee(key, ctx);
8736
+ if (found === null || found.kind !== "followed") {
8737
+ return null;
8738
+ }
8739
+ return { kind: "followed", target: runningOn(found.target, cls) };
8740
+ }
8741
+ function runningOn(target, cls) {
8742
+ const owner = ownerClassOf(target.node);
8743
+ if (owner === null || nodeId(target.file.file, owner) === cls.key) {
8744
+ return target;
8745
+ }
8746
+ return {
8747
+ ...target,
8748
+ exportPath: [cls.name, target.name],
8749
+ receiverClass: cls
8750
+ };
8751
+ }
8752
+ function classOfFunction(functionKey2, ctx) {
8753
+ const node = ctx.definitions.get(functionKey2);
8754
+ const owner = node === void 0 ? null : ownerClassOf(node);
8755
+ return owner === null ? null : subclassAt(owner, fileOfKey(functionKey2));
8756
+ }
8757
+ function onlyClassOf(key, ctx) {
8758
+ const objects = answersFor2(ctx.facts, "wantedObjectOf", key);
8759
+ const [only] = objects;
8760
+ if (objects.length !== 1 || only === void 0) {
8761
+ return null;
8762
+ }
8763
+ const file = ctx.filesByPath.get(fileOfKey(only));
8764
+ const node = file === void 0 ? null : nodeOfKey2(/* @__PURE__ */ new Map([[file.file, file.root]]), only);
8765
+ return node?.type === "class_definition" ? subclassAt(node, file?.file ?? fileOfKey(only)) : null;
8766
+ }
8767
+ function subclassAt(cls, file) {
8768
+ const name = field(cls, "name")?.text;
8769
+ return name === void 0 ? null : { key: nodeId(file, cls), name, file };
8665
8770
  }
8666
8771
  function selfCallInMixin(call, outcome, site, ctx) {
8667
8772
  if (outcome.kind !== "callerSupplied" || !outcome.key.startsWith(`${site.owner}#`)) {
@@ -8920,7 +9025,7 @@ function moduleNamed(from, module, relativeLevel, ctx) {
8920
9025
  if (file === void 0) {
8921
9026
  return null;
8922
9027
  }
8923
- noteKeyRead3(ctx.facts, file.file);
9028
+ noteKeyRead2(ctx.facts, file.file);
8924
9029
  return { kind: "module", file };
8925
9030
  }
8926
9031
 
@@ -9018,7 +9123,10 @@ function reachedFunctions(seeds, options) {
9018
9123
  passedPositions.add(position);
9019
9124
  }
9020
9125
  const called = [];
9021
- for (const target of scan.followed) {
9126
+ const calledHere = scan.followed.flatMap(
9127
+ (target) => target.receiverClass === void 0 ? [target] : [plainOf(target), target]
9128
+ );
9129
+ for (const target of calledHere) {
9022
9130
  const calleeKey2 = keyOf2(target);
9023
9131
  if (!functionByKey.has(calleeKey2)) {
9024
9132
  functionByKey.set(calleeKey2, target);
@@ -9035,11 +9143,30 @@ function reachedFunctions(seeds, options) {
9035
9143
  if (ledger === void 0) {
9036
9144
  settleBodyValues(reached, functionByKey, options);
9037
9145
  }
9146
+ const placementsOf = (key) => ({
9147
+ targets: targetsByKey.get(key),
9148
+ argTargets: argTargetsByKey.get(key),
9149
+ possibleTargets: possibleTargetsByKey.get(key)
9150
+ });
9151
+ const onSubclass = keptVariants(
9152
+ reached.flatMap(
9153
+ (key) => functionByKey.get(key)?.receiverClass === void 0 ? [] : [
9154
+ {
9155
+ key,
9156
+ differs: !sameDeclarations(
9157
+ placementsOf(key),
9158
+ placementsOf(plainKeyOf(key))
9159
+ ),
9160
+ calls: callsByKey.get(key) ?? []
9161
+ }
9162
+ ]
9163
+ )
9164
+ );
9038
9165
  const summaries = [];
9039
9166
  const summariesByKey = /* @__PURE__ */ new Map();
9040
9167
  for (const key of reached) {
9041
9168
  const target = functionByKey.get(key);
9042
- if (target === void 0) {
9169
+ if (target === void 0 || target.receiverClass !== void 0 && !onSubclass.has(key)) {
9043
9170
  continue;
9044
9171
  }
9045
9172
  const kept = replay?.summaryOf(key);
@@ -9054,6 +9181,9 @@ function reachedFunctions(seeds, options) {
9054
9181
  const summary2 = assembleSummary(libraryUnit(target, options), {
9055
9182
  gapHandling: options.gapHandling
9056
9183
  });
9184
+ if (target.receiverClass !== void 0) {
9185
+ summary2.receiverClass = displayedClass(target.receiverClass, ctx);
9186
+ }
9057
9187
  if (options.gapHandling !== "silent") {
9058
9188
  summary2.gaps.push(
9059
9189
  ...(stopsByKey.get(key) ?? []).map(unfollowedCallGap)
@@ -9191,7 +9321,8 @@ function storedScan(scan) {
9191
9321
  followed: scan.followed.map((target) => ({
9192
9322
  key: keyOf2(target),
9193
9323
  name: target.name,
9194
- exportPath: target.exportPath
9324
+ exportPath: target.exportPath,
9325
+ ...target.receiverClass === void 0 ? {} : { receiverClass: target.receiverClass }
9195
9326
  })),
9196
9327
  stops: scan.stops,
9197
9328
  targets: [...scan.targets],
@@ -9224,14 +9355,25 @@ function scanFromStored(stored, functionFor) {
9224
9355
  };
9225
9356
  }
9226
9357
  function keyOf2(target) {
9227
- return nodeId(target.file.file, target.node);
9358
+ const key = nodeId(target.file.file, target.node);
9359
+ return target.receiverClass === void 0 ? key : variantKey(key, target.receiverClass);
9360
+ }
9361
+ function plainOf(target) {
9362
+ return reachedFunctionOf(target.file, target.node);
9363
+ }
9364
+ function placeOf(target, ctx) {
9365
+ const place = { file: target.file.displayPath, span: spanOf(target.node) };
9366
+ return target.receiverClass === void 0 ? place : { ...place, receiverClass: displayedClass(target.receiverClass, ctx) };
9367
+ }
9368
+ function displayedClass(cls, ctx) {
9369
+ return {
9370
+ name: cls.name,
9371
+ file: ctx.filesByPath.get(cls.file)?.displayPath ?? cls.file
9372
+ };
9228
9373
  }
9229
- function placeCallee(placements, callee, outcome, call, file) {
9374
+ function placeCallee(placements, callee, outcome, call, file, ctx) {
9230
9375
  if (outcome.kind === "followed") {
9231
- placements.place(callee, {
9232
- file: outcome.target.file.displayPath,
9233
- span: spanOf(outcome.target.node)
9234
- });
9376
+ placements.place(callee, placeOf(outcome.target, ctx));
9235
9377
  return;
9236
9378
  }
9237
9379
  if (outcome.reason === "noDeclaration" && !isMethodCall(call)) {
@@ -9259,9 +9401,13 @@ function bodyOf(source) {
9259
9401
  }
9260
9402
  return {
9261
9403
  body,
9262
- written: callsWritten(file, body, scopeAt2(file, node), keyOf2(source), {
9263
- descendsInto: isModule(node) ? runsAtModuleLoad : outsideNestedDef
9264
- })
9404
+ written: callsWritten(
9405
+ file,
9406
+ body,
9407
+ scopeAt2(file, node),
9408
+ { owner: nodeId(file.file, node), receiverClass: source.receiverClass },
9409
+ { descendsInto: isModule(node) ? runsAtModuleLoad : outsideNestedDef }
9410
+ )
9265
9411
  };
9266
9412
  }
9267
9413
  function outsideNestedDef(node) {
@@ -9331,16 +9477,20 @@ function scanBody(source, ctx, recognized, read, implied) {
9331
9477
  followed.push(target);
9332
9478
  }
9333
9479
  };
9334
- const recordPassedArgs = (call, callee, calleeKey2) => {
9480
+ const recordPassedArgs = (call, callee, target) => {
9335
9481
  const args = field(call, "arguments");
9336
9482
  if (args === null) {
9337
9483
  return;
9338
9484
  }
9485
+ const calleeKeys = target === null ? [] : [keyOf2(target), keyOf2(plainOf(target))];
9486
+ const passes = (position) => {
9487
+ for (const calleeKey2 of calleeKeys) {
9488
+ passedPositions.add(`${calleeKey2}#${position}`);
9489
+ }
9490
+ };
9339
9491
  children(args).forEach((arg, position) => {
9340
9492
  if (arg.type === "lambda") {
9341
- if (calleeKey2 !== null) {
9342
- passedPositions.add(`${calleeKey2}#${position}`);
9343
- }
9493
+ passes(position);
9344
9494
  return;
9345
9495
  }
9346
9496
  const nameKey2 = passedNameKeyOf(file, arg);
@@ -9352,23 +9502,15 @@ function scanBody(source, ctx, recognized, read, implied) {
9352
9502
  return;
9353
9503
  }
9354
9504
  follow(resolved);
9355
- placements.placeArg(callee, position, {
9356
- file: resolved.file.displayPath,
9357
- span: spanOf(resolved.node)
9358
- });
9359
- if (calleeKey2 !== null) {
9360
- passedPositions.add(`${calleeKey2}#${position}`);
9361
- }
9505
+ placements.placeArg(callee, position, placeOf(resolved, ctx));
9506
+ passes(position);
9362
9507
  });
9363
9508
  };
9364
9509
  const recordPossible = (call, callee, dispatched) => {
9365
9510
  dispatched.targets.forEach(follow);
9366
9511
  placements.placePossible(
9367
9512
  callee,
9368
- dispatched.targets.map((target) => ({
9369
- file: target.file.displayPath,
9370
- span: spanOf(target.node)
9371
- }))
9513
+ dispatched.targets.map((target) => placeOf(target, ctx))
9372
9514
  );
9373
9515
  recordPassedArgs(call, callee, null);
9374
9516
  if (dispatched.unresolved > 0 && !recognized.has(call.id)) {
@@ -9385,11 +9527,11 @@ function scanBody(source, ctx, recognized, read, implied) {
9385
9527
  return;
9386
9528
  }
9387
9529
  const outcome = only === void 0 ? settled : { kind: "followed", target: only };
9388
- placeCallee(placements, callee, outcome, call, file);
9530
+ placeCallee(placements, callee, outcome, call, file, ctx);
9389
9531
  recordPassedArgs(
9390
9532
  call,
9391
9533
  callee,
9392
- outcome.kind === "followed" ? keyOf2(outcome.target) : null
9534
+ outcome.kind === "followed" ? outcome.target : null
9393
9535
  );
9394
9536
  if (outcome.kind === "stopped") {
9395
9537
  if (!recognized.has(call.id) && worthRecording(outcome.reason)) {
@@ -9430,12 +9572,12 @@ function scanBody(source, ctx, recognized, read, implied) {
9430
9572
  passedPositions
9431
9573
  };
9432
9574
  }
9433
- function callsWritten(file, body, outer, owner, limit) {
9575
+ function callsWritten(file, body, outer, scanned, limit) {
9434
9576
  const found = [];
9435
9577
  walkDescendants6(body, outer, children, {
9436
9578
  at: (child, scope) => {
9437
9579
  if (child.type === "call") {
9438
- found.push({ call: child, site: { file, scope, owner } });
9580
+ found.push({ call: child, site: { file, scope, ...scanned } });
9439
9581
  }
9440
9582
  },
9441
9583
  into: (child, scope) => limit.descendsInto(child) ? file.module.scopeFor.get(child.id) ?? scope : SKIP_CHILDREN5
@@ -9484,13 +9626,8 @@ function libraryUnit(target, options, binding = functionCallBinding({
9484
9626
  file.module,
9485
9627
  parameters.map((parameter) => parameter.name)
9486
9628
  );
9487
- const effects = invocationEffects(node, options.facts);
9488
- const branches = exitingBranches(
9489
- node,
9490
- file.module,
9491
- effects,
9492
- options.facts
9493
- ) ?? [
9629
+ const calls = invocationSites(node, options.facts);
9630
+ const branches = exitingBranches(node, file.module, calls, options.facts) ?? [
9494
9631
  {
9495
9632
  conditions: [],
9496
9633
  terminal: {
@@ -9505,7 +9642,7 @@ function libraryUnit(target, options, binding = functionCallBinding({
9505
9642
  emitEvent: null,
9506
9643
  location: range
9507
9644
  },
9508
- effects,
9645
+ effects: calls.map((call) => call.effect),
9509
9646
  location: range,
9510
9647
  isDefault: true
9511
9648
  }
@@ -9602,7 +9739,131 @@ function parameterName2(param) {
9602
9739
  return identifiersUnder(param)[0] ?? null;
9603
9740
  }
9604
9741
 
9742
+ // src/entryPoints.ts
9743
+ function declaredEntryUnits(file, packs, options) {
9744
+ const units = [];
9745
+ for (const pack of packs) {
9746
+ for (const entry of pack.entryPoints?.(file.file) ?? []) {
9747
+ const binding = file.module.moduleScope.bindings.get(entry.name);
9748
+ if (binding?.kind !== "functionDef") {
9749
+ continue;
9750
+ }
9751
+ for (const declared of entry.units) {
9752
+ const raw = libraryUnit(
9753
+ {
9754
+ file,
9755
+ node: binding.node,
9756
+ name: declared.name,
9757
+ exportPath: [entry.name]
9758
+ },
9759
+ options,
9760
+ declared.binding
9761
+ );
9762
+ units.push({
9763
+ ...raw,
9764
+ identity: { ...raw.identity, kind: declared.kind },
9765
+ deployableUnit: declared.deployableUnit,
9766
+ ...declared.metadata !== void 0 ? { packMetadata: declared.metadata } : {}
9767
+ });
9768
+ }
9769
+ }
9770
+ }
9771
+ return units;
9772
+ }
9773
+
9774
+ // src/importedDefinitions.ts
9775
+ import { isObserved, noteKeyRead as noteKeyRead3 } from "@suss/resolution";
9776
+ function importedDefinitionLookup(db, files) {
9777
+ const fileOfModuleScope = /* @__PURE__ */ new Map();
9778
+ const byFile = /* @__PURE__ */ new Map();
9779
+ for (const file of files) {
9780
+ fileOfModuleScope.set(file.module.moduleScope.node.id, file.file);
9781
+ byFile.set(file.file, file);
9782
+ }
9783
+ const found = /* @__PURE__ */ new Map();
9784
+ return (scope, name) => {
9785
+ const fromFile = fileOfModuleScope.get(moduleScopeOf(scope).node.id);
9786
+ if (fromFile === void 0) {
9787
+ return null;
9788
+ }
9789
+ const key = `${fromFile}#${name}`;
9790
+ let definition = found.get(key);
9791
+ if (definition === void 0) {
9792
+ definition = definitionAmong(originsOf(db, key), byFile);
9793
+ found.set(key, definition);
9794
+ } else if (isObserved(db)) {
9795
+ originsOf(db, key);
9796
+ }
9797
+ if (definition !== null) {
9798
+ noteKeyRead3(db, definition.file);
9799
+ }
9800
+ return definition;
9801
+ };
9802
+ }
9803
+ function definitionAmong(origins, byFile) {
9804
+ for (const origin of origins) {
9805
+ const file = byFile.get(origin.module);
9806
+ const binding = file?.module.moduleScope.bindings.get(origin.name);
9807
+ if (file === void 0 || binding === void 0) {
9808
+ continue;
9809
+ }
9810
+ const node = definedNodeOf(binding);
9811
+ if (node !== null) {
9812
+ return {
9813
+ node,
9814
+ file: file.file,
9815
+ moduleScope: file.module.moduleScope,
9816
+ scopeFor: file.module.scopeFor
9817
+ };
9818
+ }
9819
+ }
9820
+ return null;
9821
+ }
9822
+ function definedNodeOf(binding) {
9823
+ if (binding.kind === "classDef") {
9824
+ return binding.node;
9825
+ }
9826
+ if (binding.kind === "assignment") {
9827
+ return binding.value;
9828
+ }
9829
+ return null;
9830
+ }
9831
+
9832
+ // src/libraryEnvReads.ts
9833
+ import { libraryEnvReadMarker } from "@suss/extractor";
9834
+ function libraryEnvReadMarkers(files, packs) {
9835
+ const markers = [];
9836
+ for (const pack of packs) {
9837
+ for (const declared of pack.libraryEnvVars ?? []) {
9838
+ const importer = files.find(
9839
+ (file) => importsModule(file, declared.module)
9840
+ );
9841
+ if (importer !== void 0) {
9842
+ markers.push(
9843
+ libraryEnvReadMarker(pack.name, declared, importer.displayPath)
9844
+ );
9845
+ }
9846
+ }
9847
+ }
9848
+ return markers;
9849
+ }
9850
+ function importsModule(file, prefix) {
9851
+ for (const binding of file.module.moduleScope.bindings.values()) {
9852
+ if ((binding.kind === "import" || binding.kind === "importFrom") && binding.relativeLevel === 0 && binding.module.startsWith(prefix)) {
9853
+ return true;
9854
+ }
9855
+ }
9856
+ return false;
9857
+ }
9858
+
9605
9859
  // src/moduleSurface.ts
9860
+ import { moduleExportBinding } from "@suss/behavioral-ir";
9861
+ import {
9862
+ MODULE_SURFACE_RECOGNITION,
9863
+ moduleOfFile,
9864
+ settleModules
9865
+ } from "@suss/extractor";
9866
+ import { noteKeyRead as noteKeyRead4 } from "@suss/resolution";
9606
9867
  function settlePythonModules(modules) {
9607
9868
  return settleModules(modules, ["__init__.py"]);
9608
9869
  }
@@ -9690,7 +9951,13 @@ function exportsOf(file, node, exportName) {
9690
9951
  }
9691
9952
 
9692
9953
  // src/reuse.ts
9693
- import { EntryReuse, fileOfKey, noteLookup as noteLookup2 } from "@suss/resolution";
9954
+ import {
9955
+ EntryReuse,
9956
+ fileOfKey as fileOfKey2,
9957
+ noteLookup as noteLookup2,
9958
+ plainKeyOf as plainKeyOf2,
9959
+ variantKey as variantKey2
9960
+ } from "@suss/resolution";
9694
9961
  function seedsOfRecord(record) {
9695
9962
  return [
9696
9963
  ...record.module.seedKey === null ? [] : [record.module.seedKey],
@@ -9703,9 +9970,20 @@ function pythonEntryReuse(records, changedFiles, changes) {
9703
9970
  return new EntryReuse(records, changedFiles, changes, seedsOfRecord);
9704
9971
  }
9705
9972
  function storedFunction(target, definitions, filesByPath) {
9706
- const node = definitions.get(target.key);
9707
- const file = filesByPath.get(fileOfKey(target.key));
9708
- return node === void 0 || file === void 0 ? null : { file, node, name: target.name, exportPath: target.exportPath };
9973
+ const plainKey = plainKeyOf2(target.key);
9974
+ const node = definitions.get(plainKey);
9975
+ const file = filesByPath.get(fileOfKey2(target.key));
9976
+ const key = target.receiverClass === void 0 ? plainKey : variantKey2(plainKey, target.receiverClass);
9977
+ if (node === void 0 || file === void 0 || key !== target.key) {
9978
+ return null;
9979
+ }
9980
+ const found = {
9981
+ file,
9982
+ node,
9983
+ name: target.name,
9984
+ exportPath: target.exportPath
9985
+ };
9986
+ return target.receiverClass === void 0 ? found : { ...found, receiverClass: target.receiverClass };
9709
9987
  }
9710
9988
  var WalkReplay = class {
9711
9989
  constructor(reuse, definitions, filesByPath) {
@@ -12486,7 +12764,7 @@ function unitAround(target, pattern, reading, options) {
12486
12764
  });
12487
12765
  const continuations = reading.continuations;
12488
12766
  const terminals = bodyTerminals(body, raised, continuations);
12489
- const effects = invocationEffects(node, options.facts);
12767
+ const effects = invocationSites(node, options.facts);
12490
12768
  const extra = recognizedBodyEffects(
12491
12769
  node,
12492
12770
  file.module,
@@ -12821,7 +13099,9 @@ async function runPython(options, roots, cache, previous) {
12821
13099
  const sqlClients = options.packs.flatMap((pack) => pack.sqlClients ?? []);
12822
13100
  const modelQueries = options.packs.flatMap((pack) => pack.models ?? []);
12823
13101
  const declaredModules = settlePythonModules(options.modules);
12824
- const discovers = declaredModules.length > 0 || options.packs.some((pack) => pack.discovery.length > 0);
13102
+ const discovers = declaredModules.length > 0 || options.packs.some(
13103
+ (pack) => pack.discovery.length > 0 || pack.entryPoints !== void 0
13104
+ );
12825
13105
  const buildsReceivers = options.packs.some(
12826
13106
  (pack) => (pack.clients ?? []).some(
12827
13107
  (client) => (client.receiverConstructors ?? []).length > 0
@@ -12984,6 +13264,10 @@ async function runPython(options, roots, cache, previous) {
12984
13264
  facts: db,
12985
13265
  storageFor
12986
13266
  }),
13267
+ ...declaredEntryUnits(boundFile, options.packs, {
13268
+ facts: db,
13269
+ storageFor
13270
+ }),
12987
13271
  ...tests === null ? [] : tests.unitsIn(boundFile, { storageFor, facts: db })
12988
13272
  ];
12989
13273
  const charge = noDependencies2();
@@ -13093,7 +13377,10 @@ async function runPython(options, roots, cache, previous) {
13093
13377
  }
13094
13378
  const loadTimeReads = record !== void 0 && discoveries.get(file) === record ? null : timer.time(
13095
13379
  "discover",
13096
- () => charged(file, () => envReadEffects(root, moduleBinding, db))
13380
+ () => charged(
13381
+ file,
13382
+ () => envReadEffects(root, moduleBinding, db).map((one) => one.effect)
13383
+ )
13097
13384
  );
13098
13385
  const moduleKey = nodeId(file, root);
13099
13386
  if (!summariesBySeed.has(moduleKey)) {
@@ -13196,6 +13483,7 @@ async function runPython(options, roots, cache, previous) {
13196
13483
  });
13197
13484
  }
13198
13485
  summaries.push(...reached.summaries);
13486
+ summaries.push(...libraryEnvReadMarkers(bound, options.packs));
13199
13487
  const spend = underQuestionSpend(db);
13200
13488
  const overBudget = spend.abandoned + spend.skipped > 0;
13201
13489
  if (overBudget && reuse !== null) {