@suss/adapter-python 0.22.4 → 0.23.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
@@ -808,9 +808,7 @@ import {
808
808
  absentReading,
809
809
  ambiguousReading,
810
810
  andThenReading,
811
- enumerateOrDegrade as enumerateOrDegrade2,
812
811
  firstWrittenReading,
813
- guardsHoldOn,
814
812
  mapReading,
815
813
  unreadableReading,
816
814
  valueToReadFurtherFrom,
@@ -1032,11 +1030,11 @@ function subjectConstructions(db, valueKeys) {
1032
1030
  continue;
1033
1031
  }
1034
1032
  const deeper = calls.has(direct) ? writtenThroughCall.get(direct) : void 0;
1035
- const constructionKey = deeper ?? direct;
1033
+ const constructionKey2 = deeper ?? direct;
1036
1034
  const originOf2 = deeper === void 0 ? valueKey2 : direct;
1037
- const from = origins.get(`${originOf2}|${constructionKey}`);
1035
+ const from = origins.get(`${originOf2}|${constructionKey2}`);
1038
1036
  if (from !== void 0) {
1039
- found.set(valueKey2, { constructionKey, origins: from });
1037
+ found.set(valueKey2, { constructionKey: constructionKey2, origins: from });
1040
1038
  }
1041
1039
  }
1042
1040
  return found;
@@ -1443,8 +1441,8 @@ function emitValueFacts(db, filePath, root) {
1443
1441
  emitExpressionFacts(emitter, root);
1444
1442
  }
1445
1443
 
1446
- // src/paths/effects.ts
1447
- import { enumerateOrDegrade, sharedGatingConditions } from "@suss/extractor";
1444
+ // src/paths/bodyBranches.ts
1445
+ import { enumerateOrDegrade, guardsHoldOn } from "@suss/extractor";
1448
1446
 
1449
1447
  // src/paths/lowering.ts
1450
1448
  var EXIT_KINDS = {
@@ -1719,7 +1717,89 @@ function predicateOf(node) {
1719
1717
  return opaqueOf(node);
1720
1718
  }
1721
1719
 
1720
+ // src/paths/bodyBranches.ts
1721
+ function returnStatements(node, found = []) {
1722
+ for (const child of node?.namedChildren ?? []) {
1723
+ if (child === null) {
1724
+ continue;
1725
+ }
1726
+ if (child.type === "function_definition" || child.type === "lambda") {
1727
+ continue;
1728
+ }
1729
+ if (child.type === "return_statement") {
1730
+ found.push(child);
1731
+ }
1732
+ returnStatements(child, found);
1733
+ }
1734
+ return found;
1735
+ }
1736
+ function bodyTerminals(body, raised, continuations = []) {
1737
+ const handsOn = new NodeSet(continuations);
1738
+ const found = [
1739
+ ...returnStatements(body).filter((statement) => !handsOn.has(statement)).map((statement) => ({ type: "return", statement })),
1740
+ ...raised.map(
1741
+ (response) => ({
1742
+ type: "raise",
1743
+ statement: response.statement,
1744
+ terminal: response.terminal
1745
+ })
1746
+ ),
1747
+ ...continuations.map(
1748
+ (statement) => ({ type: "continuation", statement })
1749
+ )
1750
+ ];
1751
+ return found.sort((a, b) => a.statement.startIndex - b.statement.startIndex);
1752
+ }
1753
+ function effectsReaching(effects, conditions) {
1754
+ return effects.filter(
1755
+ (effect) => guardsHoldOn(effect.preconditions, conditions)
1756
+ );
1757
+ }
1758
+ function enumerateBodyBranches(options) {
1759
+ const statements = options.terminals.map((found) => found.statement);
1760
+ const leavesByCall = new NodeSet([
1761
+ ...options.raised.filter((response) => response.thrownByCall).map((response) => response.statement),
1762
+ ...options.terminals.filter((found) => found.type === "continuation").map((found) => found.statement)
1763
+ ]);
1764
+ const lowered = lowerPythonBody(options.body, statements, leavesByCall);
1765
+ const enumerated = enumerateOrDegrade(
1766
+ {
1767
+ statements: lowered.statements,
1768
+ terminalsByStmt: lowered.terminalsByStmt
1769
+ },
1770
+ statements
1771
+ );
1772
+ const branches = [];
1773
+ const push = (branch, paths) => {
1774
+ for (const path6 of paths) {
1775
+ const conditions = path6.map((condition) => ({
1776
+ sourceText: condition.sourceText,
1777
+ structured: condition.expression === null ? null : predicateOf(condition.expression),
1778
+ polarity: condition.polarity,
1779
+ source: condition.source
1780
+ }));
1781
+ branches.push({
1782
+ ...branch,
1783
+ conditions,
1784
+ effects: effectsReaching(options.effects, conditions),
1785
+ isDefault: path6.length === 0
1786
+ });
1787
+ }
1788
+ };
1789
+ for (const found of options.terminals) {
1790
+ const paths = enumerated.byTerminal.get(found.statement);
1791
+ if (paths !== void 0) {
1792
+ push(options.branchOf(found), paths);
1793
+ }
1794
+ }
1795
+ if (options.fallthrough !== void 0) {
1796
+ push(options.fallthrough, enumerated.fallthrough);
1797
+ }
1798
+ return branches;
1799
+ }
1800
+
1722
1801
  // src/paths/effects.ts
1802
+ import { enumerateOrDegrade as enumerateOrDegrade2, sharedGatingConditions } from "@suss/extractor";
1723
1803
  var NESTED_DEFINITION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
1724
1804
  var LITERAL_ARGS = {
1725
1805
  string: (node) => ({ kind: "string", value: node.text.slice(1, -1) }),
@@ -1798,7 +1878,7 @@ function invocationEffects(definitionNode) {
1798
1878
  statements.push(statement);
1799
1879
  }
1800
1880
  const lowered = lowerPythonBody(body, statements);
1801
- const enumerated = enumerateOrDegrade(
1881
+ const enumerated = enumerateOrDegrade2(
1802
1882
  {
1803
1883
  statements: lowered.statements,
1804
1884
  terminalsByStmt: lowered.terminalsByStmt
@@ -2708,6 +2788,18 @@ function raisedResponseOf(statement, options) {
2708
2788
  terminal: terminalOf(statement, callee, call, declared, options.facts)
2709
2789
  };
2710
2790
  }
2791
+ function returnedResponseStatus(statement, options) {
2792
+ const returned = statement.namedChildren[0];
2793
+ if (options.calls.length === 0 || returned === void 0 || returned === null || returned.type !== "call") {
2794
+ return null;
2795
+ }
2796
+ const callee = field(returned, "function");
2797
+ if (callee === null) {
2798
+ return null;
2799
+ }
2800
+ const declared = declaredCallFor(callee, options);
2801
+ return declared === void 0 ? null : statusOf(declared, returned, options.facts);
2802
+ }
2711
2803
  function calleeOf(expression, call) {
2712
2804
  return call === null ? expression : field(call, "function") ?? expression;
2713
2805
  }
@@ -3542,7 +3634,7 @@ function classRouteUnits(pattern, pack, classification, classNode, module, optio
3542
3634
  routePath: entry.routePath,
3543
3635
  ...entry.mount !== void 0 ? { mount: entry.mount } : {},
3544
3636
  requestBodyFromAnnotatedClass: pattern.annotatedClassIsRequestBody === true,
3545
- readsReturnedStatus: pattern.statusFromReturnedTuple === true,
3637
+ pattern,
3546
3638
  responseStatusCalls: pattern.responseStatusCalls ?? [],
3547
3639
  facts: options.facts,
3548
3640
  injectedCallees: new Set(pattern.injectedParameterCallees ?? []),
@@ -3553,10 +3645,21 @@ function classRouteUnits(pattern, pack, classification, classNode, module, optio
3553
3645
  skipReceiverParam: true,
3554
3646
  responseShape: absentReading,
3555
3647
  statusCode: defaultedStatus(
3556
- readReturnedStatus(pattern, maybeMethod),
3648
+ readReturnedStatus(pattern, maybeMethod, {
3649
+ module,
3650
+ facts: options.facts
3651
+ }),
3557
3652
  pattern
3558
3653
  ),
3559
- storage: options.storage
3654
+ storage: options.storage,
3655
+ wrappers: wrappersAround(
3656
+ pattern,
3657
+ pack,
3658
+ classification,
3659
+ maybeMethod,
3660
+ module,
3661
+ options
3662
+ )
3560
3663
  },
3561
3664
  options
3562
3665
  )
@@ -3686,21 +3789,6 @@ function readStatusCode(pattern, classification) {
3686
3789
  classification.range
3687
3790
  );
3688
3791
  }
3689
- function returnStatements(node, found = []) {
3690
- for (const child of node?.namedChildren ?? []) {
3691
- if (child === null) {
3692
- continue;
3693
- }
3694
- if (child.type === "function_definition" || child.type === "lambda") {
3695
- continue;
3696
- }
3697
- if (child.type === "return_statement") {
3698
- found.push(child);
3699
- }
3700
- returnStatements(child, found);
3701
- }
3702
- return found;
3703
- }
3704
3792
  function statusFromReturnedValue(node) {
3705
3793
  if (node === void 0) {
3706
3794
  return null;
@@ -3724,27 +3812,19 @@ function statusOfReturn(statement) {
3724
3812
  const status = statusFromReturnedValue(returned.namedChildren[1]);
3725
3813
  return status === null ? { kind: "unreadable" } : { kind: "status", value: status };
3726
3814
  }
3727
- function effectsReaching(effects, conditions) {
3728
- return effects.filter(
3729
- (effect) => guardsHoldOn(effect.preconditions, conditions)
3730
- );
3731
- }
3732
3815
  function returnStatusReading(statement, ctx) {
3733
- if (!ctx.readsReturnedStatus) {
3734
- return ctx.declaredStatus.reading;
3735
- }
3736
- const read = statusOfReturn(statement);
3816
+ const read = statusOfReturnIn(statement, ctx.pattern, ctx);
3737
3817
  const range = rangeOf(statement);
3738
3818
  if (read.kind === "status") {
3739
3819
  return writtenReading(read.value, range);
3740
3820
  }
3741
- if (read.kind === "none") {
3742
- return absentReading;
3821
+ if (read.kind === "unreadable") {
3822
+ return unreadableReading(
3823
+ "This return gives a status this reading cannot resolve to a number, so this outcome claims none",
3824
+ range
3825
+ );
3743
3826
  }
3744
- return unreadableReading(
3745
- "This return gives a status this reading cannot resolve to a number, so this outcome claims none",
3746
- range
3747
- );
3827
+ return ctx.pattern.statusFromReturnedTuple === true ? absentReading : ctx.declaredStatus.reading;
3748
3828
  }
3749
3829
  function returnBranch(statement, ctx) {
3750
3830
  const range = rangeOf(statement);
@@ -3780,74 +3860,49 @@ function terminalBranchOf(found, ctx) {
3780
3860
  raise: (variant) => ({
3781
3861
  terminal: variant.terminal,
3782
3862
  location: rangeOf(variant.statement)
3783
- })
3863
+ }),
3864
+ // A route has no continuation, so none is ever collected for it.
3865
+ continuation: (variant) => returnBranch(variant.statement, ctx)
3784
3866
  };
3785
3867
  return dispatchByType(table, found);
3786
3868
  }
3787
- function routeTerminals(body, raised) {
3788
- const found = [
3789
- ...returnStatements(body).map(
3790
- (statement) => ({ type: "return", statement })
3791
- ),
3792
- ...raised.map(
3793
- (response) => ({
3794
- type: "raise",
3795
- statement: response.statement,
3796
- terminal: response.terminal
3797
- })
3798
- )
3799
- ];
3800
- return found.sort((a, b) => a.statement.startIndex - b.statement.startIndex);
3801
- }
3802
3869
  function branchesPerTerminal(options) {
3803
3870
  const body = field(options.definitionNode, "body");
3804
3871
  const raised = raisedResponses(body, {
3805
3872
  calls: options.responseStatusCalls,
3873
+ module: options.ctx.module,
3874
+ facts: options.ctx.facts
3875
+ });
3876
+ const terminals = bodyTerminals(body, raised);
3877
+ const soleReturnWritesBody = terminals.length === 1 && terminals[0]?.type === "return" && returnedBodyShape(terminals[0].statement) !== null;
3878
+ if (terminals.length < 2 && !soleReturnWritesBody) {
3879
+ return null;
3880
+ }
3881
+ const branches = enumerateBodyBranches({
3882
+ body,
3883
+ terminals,
3884
+ raised,
3885
+ effects: options.effects,
3886
+ branchOf: (found) => terminalBranchOf(found, options.ctx)
3887
+ });
3888
+ return branches.length > 1 || soleReturnWritesBody ? branches : null;
3889
+ }
3890
+ function statusOfReturnIn(statement, pattern, options) {
3891
+ const constructed = returnedResponseStatus(statement, {
3892
+ calls: pattern.responseConstructors ?? [],
3806
3893
  module: options.module,
3807
3894
  facts: options.facts
3808
3895
  });
3809
- const terminals = routeTerminals(body, raised);
3810
- if (terminals.length < 2) {
3811
- return null;
3896
+ if (constructed !== null) {
3897
+ return constructed.type === "literal" ? { kind: "status", value: constructed.value } : { kind: "unreadable" };
3812
3898
  }
3813
- const statements = terminals.map((found) => found.statement);
3814
- const thrownByCall = new NodeSet(
3815
- raised.filter((response) => response.thrownByCall).map((response) => response.statement)
3816
- );
3817
- const lowered = lowerPythonBody(body, statements, thrownByCall);
3818
- const enumerated = enumerateOrDegrade2(
3819
- {
3820
- statements: lowered.statements,
3821
- terminalsByStmt: lowered.terminalsByStmt
3822
- },
3823
- statements
3824
- );
3825
- const branches = [];
3826
- for (const found of terminals) {
3827
- const paths = enumerated.byTerminal.get(found.statement);
3828
- if (paths === void 0) {
3829
- continue;
3830
- }
3831
- const branch = terminalBranchOf(found, options.ctx);
3832
- for (const path6 of paths) {
3833
- const conditions = path6.map((condition) => ({
3834
- sourceText: condition.sourceText,
3835
- structured: condition.expression === null ? null : predicateOf(condition.expression),
3836
- polarity: condition.polarity,
3837
- source: condition.source
3838
- }));
3839
- branches.push({
3840
- ...branch,
3841
- conditions,
3842
- effects: effectsReaching(options.effects, conditions),
3843
- isDefault: path6.length === 0
3844
- });
3845
- }
3899
+ if (pattern.statusFromReturnedTuple !== true) {
3900
+ return { kind: "none" };
3846
3901
  }
3847
- return branches.length > 1 ? branches : null;
3902
+ return statusOfReturn(statement);
3848
3903
  }
3849
- function readReturnedStatus(pattern, definitionNode) {
3850
- if (pattern.statusFromReturnedTuple !== true) {
3904
+ function readReturnedStatus(pattern, definitionNode, options) {
3905
+ if (pattern.statusFromReturnedTuple !== true && (pattern.responseConstructors ?? []).length === 0) {
3851
3906
  return absentReading;
3852
3907
  }
3853
3908
  const range = rangeOf(definitionNode);
@@ -3855,7 +3910,7 @@ function readReturnedStatus(pattern, definitionNode) {
3855
3910
  let plainReturns = 0;
3856
3911
  let unreadable = 0;
3857
3912
  for (const statement of returnStatements(field(definitionNode, "body"))) {
3858
- const read = statusOfReturn(statement);
3913
+ const read = statusOfReturnIn(statement, pattern, options);
3859
3914
  if (read.kind === "status") {
3860
3915
  statuses.add(read.value);
3861
3916
  } else if (read.kind === "none") {
@@ -3943,7 +3998,7 @@ function functionRouteUnits(pattern, pack, verb, classification, functionNode, m
3943
3998
  routePath: entry.routePath,
3944
3999
  ...entry.mount !== void 0 ? { mount: entry.mount } : {},
3945
4000
  requestBodyFromAnnotatedClass: pattern.annotatedClassIsRequestBody === true,
3946
- readsReturnedStatus: pattern.statusFromReturnedTuple === true,
4001
+ pattern,
3947
4002
  responseStatusCalls: pattern.responseStatusCalls ?? [],
3948
4003
  facts: options.facts,
3949
4004
  injectedCallees: new Set(pattern.injectedParameterCallees ?? []),
@@ -3956,17 +4011,41 @@ function functionRouteUnits(pattern, pack, verb, classification, functionNode, m
3956
4011
  statusCode: defaultedStatus(
3957
4012
  declaredOrReturnedStatus(
3958
4013
  readStatusCode(pattern, classification),
3959
- readReturnedStatus(pattern, functionNode)
4014
+ readReturnedStatus(pattern, functionNode, {
4015
+ module,
4016
+ facts: options.facts
4017
+ })
3960
4018
  ),
3961
4019
  pattern
3962
4020
  ),
3963
4021
  definitionsCtx: ctx,
3964
- storage: options.storage
4022
+ storage: options.storage,
4023
+ wrappers: wrappersAround(
4024
+ pattern,
4025
+ pack,
4026
+ classification,
4027
+ functionNode,
4028
+ module,
4029
+ options
4030
+ )
3965
4031
  },
3966
4032
  options
3967
4033
  )
3968
4034
  );
3969
4035
  }
4036
+ function wrappersAround(pattern, pack, classification, definitionNode, module, options) {
4037
+ if (options.wrappers === void 0) {
4038
+ return [];
4039
+ }
4040
+ return options.wrappers.wrappersFor({
4041
+ pack,
4042
+ pattern,
4043
+ file: options.absoluteFile ?? options.filePath,
4044
+ module,
4045
+ classification,
4046
+ definitionNode
4047
+ });
4048
+ }
3970
4049
  function readResponseShape(declared, readAnnotation) {
3971
4050
  if (declared.kind === "written") {
3972
4051
  return { reading: declared, passedOver: [] };
@@ -4026,7 +4105,7 @@ function buildRouteUnit(options) {
4026
4105
  method,
4027
4106
  routePath,
4028
4107
  requestBodyFromAnnotatedClass,
4029
- readsReturnedStatus,
4108
+ pattern,
4030
4109
  injectedCallees,
4031
4110
  definitionNode,
4032
4111
  enclosingScope,
@@ -4057,10 +4136,14 @@ function buildRouteUnit(options) {
4057
4136
  const perTerminal = branchesPerTerminal({
4058
4137
  definitionNode,
4059
4138
  effects,
4060
- module,
4061
- facts: options.facts,
4062
4139
  responseStatusCalls: options.responseStatusCalls,
4063
- ctx: { readsReturnedStatus, responseShape, declaredStatus: statusCode }
4140
+ ctx: {
4141
+ pattern,
4142
+ responseShape,
4143
+ declaredStatus: statusCode,
4144
+ module,
4145
+ facts: options.facts
4146
+ }
4064
4147
  });
4065
4148
  const branches = (perTerminal ?? []).map(
4066
4149
  (branch) => extra.length === 0 ? branch : { ...branch, extraEffects: extra }
@@ -4119,7 +4202,8 @@ function buildRouteUnit(options) {
4119
4202
  ...responseShape.passedOver
4120
4203
  ],
4121
4204
  ...collectedDefinitions(ctx) !== null ? { definitions: collectedDefinitions(ctx) } : {},
4122
- ...options.mount !== void 0 ? { mount: options.mount } : {}
4205
+ ...options.mount !== void 0 ? { mount: options.mount } : {},
4206
+ ...options.wrappers !== void 0 && options.wrappers.length > 0 ? { wrappers: options.wrappers } : {}
4123
4207
  };
4124
4208
  }
4125
4209
  function unreadRoleReadings(parameters, range) {
@@ -4479,6 +4563,7 @@ import {
4479
4563
  import { Database as Database2 } from "@suss/datalog";
4480
4564
  import {
4481
4565
  assembleSummary as assembleSummary2,
4566
+ composeWrappers,
4482
4567
  createCacheLayer,
4483
4568
  createTimer,
4484
4569
  moduleInitStructure,
@@ -5201,12 +5286,8 @@ function buildRouterIndex(files, packs, resolverOptions) {
5201
5286
  }
5202
5287
  return {
5203
5288
  moduleDef(fromFile, spec, name) {
5204
- const resolution = resolveModule(fromFile, spec, resolverOptions);
5205
- if (resolution.status !== "resolved") {
5206
- return null;
5207
- }
5208
- const bound = byFile.get(resolution.file);
5209
- if (bound === void 0) {
5289
+ const bound = boundModuleAt(fromFile, spec, byFile, resolverOptions);
5290
+ if (bound === null) {
5210
5291
  return null;
5211
5292
  }
5212
5293
  const binding = bound.module.moduleScope.bindings.get(name);
@@ -5232,12 +5313,12 @@ function buildRouterIndex(files, packs, resolverOptions) {
5232
5313
  }
5233
5314
  return composedPrefixOf(index, construction, displayPaths.get(module));
5234
5315
  },
5235
- resolveConstruction(pattern, module, constructorName, constructionKey) {
5316
+ resolveConstruction(pattern, module, constructorName, constructionKey2) {
5236
5317
  const index = byPattern.get(pattern);
5237
5318
  if (index === void 0) {
5238
5319
  return NOT_ROUTER;
5239
5320
  }
5240
- const construction = index.byValueKey.get(constructionKey);
5321
+ const construction = index.byValueKey.get(constructionKey2);
5241
5322
  if (construction !== void 0) {
5242
5323
  return composedPrefixOf(index, construction, displayPaths.get(module));
5243
5324
  }
@@ -5473,6 +5554,13 @@ function buildPatternIndex(files, importModule, composition, resolverOptions) {
5473
5554
  }
5474
5555
  return index;
5475
5556
  }
5557
+ function boundModuleAt(fromFile, spec, byFile, resolverOptions) {
5558
+ const resolution = resolveModule(fromFile, spec, resolverOptions);
5559
+ if (resolution.status !== "resolved") {
5560
+ return null;
5561
+ }
5562
+ return byFile.get(resolution.file) ?? null;
5563
+ }
5476
5564
  function constructionOf(name, scope, importModule) {
5477
5565
  const binding = resolveName(scope, name);
5478
5566
  if (binding?.kind !== "assignment" || binding.value?.type !== "call") {
@@ -5681,16 +5769,13 @@ function constructionNamed(name, scope, scan, constructions = scan.index.constru
5681
5769
  if (binding.kind !== "importFrom") {
5682
5770
  return null;
5683
5771
  }
5684
- const resolution = resolveModule(
5772
+ const target = boundModuleAt(
5685
5773
  scan.bound.file,
5686
5774
  { module: binding.module, relativeLevel: binding.relativeLevel },
5775
+ scan.byFile,
5687
5776
  scan.resolverOptions
5688
5777
  );
5689
- if (resolution.status !== "resolved") {
5690
- return null;
5691
- }
5692
- const target = scan.byFile.get(resolution.file);
5693
- if (target === void 0) {
5778
+ if (target === null) {
5694
5779
  return null;
5695
5780
  }
5696
5781
  return constructions.get(target.module)?.get(binding.importedName) ?? null;
@@ -5708,10 +5793,10 @@ function constructionThroughFacts(name, scope, scan) {
5708
5793
  return null;
5709
5794
  }
5710
5795
  const nameKey = `${scan.bound.file}#${name}`;
5711
- const constructionKey = subjectConstructions(facts, [nameKey]).get(
5796
+ const constructionKey2 = subjectConstructions(facts, [nameKey]).get(
5712
5797
  nameKey
5713
5798
  )?.constructionKey;
5714
- return constructionKey === void 0 ? null : scan.index.byValueKey.get(constructionKey) ?? null;
5799
+ return constructionKey2 === void 0 ? null : scan.index.byValueKey.get(constructionKey2) ?? null;
5715
5800
  }
5716
5801
  var MOUNT_STATE_BY_EFFECT = {
5717
5802
  prefixes: (statedPrefix, site) => ({
@@ -5948,12 +6033,13 @@ function constructionThroughModule(arg, scope, scan) {
5948
6033
  if (spec === null) {
5949
6034
  return null;
5950
6035
  }
5951
- const resolution = resolveModule(scan.bound.file, spec, scan.resolverOptions);
5952
- if (resolution.status !== "resolved") {
5953
- return null;
5954
- }
5955
- const target = scan.byFile.get(resolution.file);
5956
- if (target === void 0) {
6036
+ const target = boundModuleAt(
6037
+ scan.bound.file,
6038
+ spec,
6039
+ scan.byFile,
6040
+ scan.resolverOptions
6041
+ );
6042
+ if (target === null) {
5957
6043
  return null;
5958
6044
  }
5959
6045
  return scan.index.constructions.get(target.module)?.get(arg.attributeName) ?? null;
@@ -6304,6 +6390,472 @@ var adapterStamp = createAdapterStamp({
6304
6390
  version: ADAPTER_VERSION
6305
6391
  });
6306
6392
 
6393
+ // src/wrappers.ts
6394
+ import { absentReading as absentReading2, walkDescendants as walkDescendants4 } from "@suss/extractor";
6395
+ function buildWrapperIndex(files, options) {
6396
+ const forms = declaredForms(options.packs);
6397
+ const filesByPath = new Map(files.map((file) => [file.file, file]));
6398
+ const index = new PythonWrapperIndex(options, filesByPath);
6399
+ if (forms.length > 0) {
6400
+ for (const file of files) {
6401
+ for (const declared of forms) {
6402
+ registerFrom(file, declared, index);
6403
+ }
6404
+ }
6405
+ }
6406
+ return index;
6407
+ }
6408
+ function declaredForms(packs) {
6409
+ return packs.flatMap(
6410
+ (pack) => pack.discovery.flatMap(
6411
+ (pattern) => (pattern.wrappers ?? []).map((form) => ({ pack, pattern, form }))
6412
+ )
6413
+ );
6414
+ }
6415
+ var PythonWrapperIndex = class {
6416
+ constructor(options, filesByPath) {
6417
+ this.options = options;
6418
+ this.filesByPath = filesByPath;
6419
+ }
6420
+ options;
6421
+ filesByPath;
6422
+ /** By pack name: what the app registered, in the order the forms are declared and then the order the files were read. */
6423
+ everyRoute = /* @__PURE__ */ new Map();
6424
+ /** By construction key: what a router or a blueprint registered. */
6425
+ ownRoutes = /* @__PURE__ */ new Map();
6426
+ unitsByKey = /* @__PURE__ */ new Map();
6427
+ unitsByFile = /* @__PURE__ */ new Map();
6428
+ register(covers, target, declared) {
6429
+ const registered = this.registered(target, declared);
6430
+ const table = covers.kind === "everyRoute" ? this.everyRoute : this.ownRoutes;
6431
+ const key = covers.kind === "everyRoute" ? covers.pack : covers.key;
6432
+ const list = table.get(key) ?? [];
6433
+ list.push(registered);
6434
+ table.set(key, list);
6435
+ }
6436
+ /** The unit for a function, built once however many registrations point at it. */
6437
+ registered(target, declared) {
6438
+ const span = spanOf(target.node);
6439
+ const key = `${target.file.file}:${span.start}-${span.end}`;
6440
+ if (!this.unitsByKey.has(key)) {
6441
+ const unit = wrapperUnit(target, declared, this.options);
6442
+ this.unitsByKey.set(key, unit);
6443
+ const inFile = this.unitsByFile.get(target.file.file) ?? [];
6444
+ inFile.push(unit);
6445
+ this.unitsByFile.set(target.file.file, inFile);
6446
+ }
6447
+ return {
6448
+ reference: {
6449
+ file: target.file.displayPath,
6450
+ name: target.name,
6451
+ ...isThrowForm(declared.form) ? { onThrow: true } : {}
6452
+ },
6453
+ form: declared.form
6454
+ };
6455
+ }
6456
+ wrappersFor(query) {
6457
+ const file = this.filesByPath.get(query.file);
6458
+ const forms = (query.pattern.wrappers ?? []).map((form) => ({
6459
+ pack: query.pack,
6460
+ pattern: query.pattern,
6461
+ form
6462
+ }));
6463
+ if (file === void 0 || forms.length === 0) {
6464
+ return [];
6465
+ }
6466
+ const found = [
6467
+ ...this.everyRoute.get(query.pack.name) ?? [],
6468
+ ...this.ownRoutesOf(query)
6469
+ ];
6470
+ for (const declared of forms) {
6471
+ if (declared.form.type !== "dependency") {
6472
+ continue;
6473
+ }
6474
+ const site = {
6475
+ file,
6476
+ scope: query.module.moduleScope,
6477
+ rebound: /* @__PURE__ */ new Set()
6478
+ };
6479
+ const targets = [
6480
+ ...decoratorDependencies(query.classification, declared.form),
6481
+ ...parameterDependencies(query.definitionNode, declared.form)
6482
+ ];
6483
+ for (const name of targets) {
6484
+ const target = resolveNamedFunctionArgument(name, site, {
6485
+ filesByPath: this.filesByPath,
6486
+ roots: this.options.roots
6487
+ });
6488
+ if (target !== null) {
6489
+ found.push(this.registered(target, declared));
6490
+ }
6491
+ }
6492
+ }
6493
+ const seen = /* @__PURE__ */ new Set();
6494
+ const ordered = [];
6495
+ for (const wrapper of [...found].sort(
6496
+ (a, b) => runOrder(a.form) - runOrder(b.form)
6497
+ )) {
6498
+ const key = `${wrapper.reference.file}::${wrapper.reference.name}::${wrapper.reference.onThrow === true}`;
6499
+ if (seen.has(key)) {
6500
+ continue;
6501
+ }
6502
+ seen.add(key);
6503
+ ordered.push(wrapper.reference);
6504
+ }
6505
+ return ordered;
6506
+ }
6507
+ ownRoutesOf(query) {
6508
+ const site = this.objectSiteOf(query);
6509
+ if (site === null) {
6510
+ return [];
6511
+ }
6512
+ for (const form of query.pattern.wrappers ?? []) {
6513
+ const match = registrarOf(site.name, site.scope, {
6514
+ pack: query.pack,
6515
+ pattern: query.pattern,
6516
+ form
6517
+ });
6518
+ if (match !== null && match.registrar.covers === "ownRoutes") {
6519
+ return this.ownRoutes.get(constructionKey(site.file, match.call)) ?? [];
6520
+ }
6521
+ }
6522
+ return [];
6523
+ }
6524
+ /**
6525
+ * Where the decorator's object was built: the route's own file, the file
6526
+ * a project wrapper function is written in, or the file an imported
6527
+ * router comes from. The name is the one that file binds it under.
6528
+ */
6529
+ objectSiteOf(query) {
6530
+ const name = query.classification.objectName;
6531
+ if (name === null) {
6532
+ return null;
6533
+ }
6534
+ const wrapperModule = query.classification.objectModule;
6535
+ if (wrapperModule !== void 0) {
6536
+ const file = this.fileOfModule(wrapperModule);
6537
+ return file === null ? null : { file, scope: wrapperModule.moduleScope, name };
6538
+ }
6539
+ const binding = resolveName(query.module.moduleScope, name);
6540
+ if (binding?.kind !== "importFrom") {
6541
+ return { file: query.file, scope: query.module.moduleScope, name };
6542
+ }
6543
+ const target = boundModuleAt(
6544
+ query.file,
6545
+ { module: binding.module, relativeLevel: binding.relativeLevel },
6546
+ this.filesByPath,
6547
+ { roots: this.options.roots }
6548
+ );
6549
+ return target === null ? null : {
6550
+ file: target.file,
6551
+ scope: target.module.moduleScope,
6552
+ name: binding.importedName
6553
+ };
6554
+ }
6555
+ fileOfModule(module) {
6556
+ for (const file of this.filesByPath.values()) {
6557
+ if (file.module === module) {
6558
+ return file.file;
6559
+ }
6560
+ }
6561
+ return null;
6562
+ }
6563
+ unitsIn(file) {
6564
+ return this.unitsByFile.get(file) ?? [];
6565
+ }
6566
+ };
6567
+ function isThrowForm(form) {
6568
+ return form.type === "decoratedWrapper" && form.throwParam !== void 0;
6569
+ }
6570
+ function runOrder(form) {
6571
+ if (isThrowForm(form)) {
6572
+ return 2;
6573
+ }
6574
+ return form.type === "decoratedWrapper" ? 0 : 1;
6575
+ }
6576
+ function registrarOf(name, scope, declared) {
6577
+ const modules = [
6578
+ ...declared.pattern.importModule,
6579
+ ...declared.form.registrars.flatMap(
6580
+ (registrar2) => registrar2.importModule ?? []
6581
+ )
6582
+ ];
6583
+ const construction = constructionOf(name, scope, modules);
6584
+ if (construction === null) {
6585
+ return null;
6586
+ }
6587
+ const registrar = declared.form.registrars.find(
6588
+ (candidate) => candidate.constructorName === construction.constructorName
6589
+ );
6590
+ return registrar === void 0 ? null : { registrar, call: construction.call };
6591
+ }
6592
+ function constructionKey(file, call) {
6593
+ return `${file}#${call.startIndex}`;
6594
+ }
6595
+ function registerFrom(file, declared, index) {
6596
+ const table = {
6597
+ dependency: () => registerConstructorDependencies(
6598
+ file,
6599
+ declared,
6600
+ declared.form,
6601
+ index
6602
+ ),
6603
+ decoratedWrapper: () => registerDecorated(
6604
+ file,
6605
+ declared,
6606
+ declared.form,
6607
+ index
6608
+ )
6609
+ };
6610
+ table[declared.form.type]();
6611
+ }
6612
+ function registerConstructorDependencies(file, declared, form, index) {
6613
+ const scope = file.module.moduleScope;
6614
+ for (const stmt of file.root.namedChildren) {
6615
+ const assignment2 = stmt?.type === "expression_statement" ? stmt.namedChildren.find((child) => child?.type === "assignment") : void 0;
6616
+ const left = assignment2 === void 0 || assignment2 === null ? null : field(assignment2, "left");
6617
+ if (left?.type !== "identifier") {
6618
+ continue;
6619
+ }
6620
+ const match = registrarOf(left.text, scope, declared);
6621
+ if (match === null) {
6622
+ continue;
6623
+ }
6624
+ const { keywordArgs } = readCallArguments(field(match.call, "arguments"));
6625
+ const listed = keywordArgs[form.keyword];
6626
+ if (listed === void 0) {
6627
+ continue;
6628
+ }
6629
+ const site = { file, scope, rebound: /* @__PURE__ */ new Set() };
6630
+ for (const name of dependencyNamesIn(listed.node, form)) {
6631
+ const target = resolveNamedFunctionArgument(name, site, {
6632
+ filesByPath: index.filesByPath,
6633
+ roots: index.options.roots
6634
+ });
6635
+ if (target !== null) {
6636
+ index.register(
6637
+ coverageOf(match, file, declared.pack),
6638
+ target,
6639
+ declared
6640
+ );
6641
+ }
6642
+ }
6643
+ }
6644
+ }
6645
+ function coverageOf(match, file, pack) {
6646
+ return match.registrar.covers === "everyRoute" ? { kind: "everyRoute", pack: pack.name } : { kind: "ownRoutes", key: constructionKey(file.file, match.call) };
6647
+ }
6648
+ function registerDecorated(file, declared, form, index) {
6649
+ walkDescendants4(file.root, file.module.moduleScope, {
6650
+ at: (node, scope) => {
6651
+ if (node.type !== "decorated_definition") {
6652
+ return;
6653
+ }
6654
+ const { definition, decorators } = stripDecorators(node);
6655
+ if (definition.type !== "function_definition") {
6656
+ return;
6657
+ }
6658
+ for (const decorator of decorators) {
6659
+ const receiver = decoratorReceiver(decorator);
6660
+ if (receiver === null || receiver.attributeName !== form.attribute || receiver.object.type !== "identifier") {
6661
+ continue;
6662
+ }
6663
+ const match = registrarOf(receiver.object.text, scope, declared);
6664
+ const name = field(definition, "name")?.text;
6665
+ if (match === null || name === void 0) {
6666
+ continue;
6667
+ }
6668
+ index.register(
6669
+ coverageOf(match, file, declared.pack),
6670
+ { file, node: definition, name, exportPath: [name] },
6671
+ declared
6672
+ );
6673
+ }
6674
+ },
6675
+ into: (node, scope) => file.module.scopeFor.get(node.id) ?? scope
6676
+ });
6677
+ }
6678
+ function dependencyNamesIn(node, form) {
6679
+ const calls = node.type === "list" ? node.namedChildren : [node];
6680
+ const names = [];
6681
+ for (const call of calls) {
6682
+ const name = call === null ? null : dependencyNameOf(call, form);
6683
+ if (name !== null) {
6684
+ names.push(name);
6685
+ }
6686
+ }
6687
+ return names;
6688
+ }
6689
+ function dependencyNameOf(node, form) {
6690
+ if (node.type !== "call") {
6691
+ return null;
6692
+ }
6693
+ const callee = field(node, "function");
6694
+ const calleeName3 = callee?.type === "identifier" ? callee.text : callee?.type === "attribute" ? field(callee, "attribute")?.text ?? null : null;
6695
+ if (calleeName3 === null || !form.callees.includes(calleeName3)) {
6696
+ return null;
6697
+ }
6698
+ const first = field(node, "arguments")?.namedChildren[0];
6699
+ return first?.type === "identifier" ? first.text : null;
6700
+ }
6701
+ function decoratorDependencies(classification, form) {
6702
+ const listed = classification.keywordArgs[form.keyword];
6703
+ return listed === void 0 ? [] : dependencyNamesIn(listed.node, form);
6704
+ }
6705
+ var MAX_ANNOTATION_DEPTH2 = 6;
6706
+ function parameterDependencies(definitionNode, form) {
6707
+ const names = [];
6708
+ const findIn = (node, depth) => {
6709
+ if (node === null || depth > MAX_ANNOTATION_DEPTH2) {
6710
+ return null;
6711
+ }
6712
+ const direct = dependencyNameOf(node, form);
6713
+ if (direct !== null) {
6714
+ return direct;
6715
+ }
6716
+ for (const child of node.namedChildren) {
6717
+ const found = findIn(child, depth + 1);
6718
+ if (found !== null) {
6719
+ return found;
6720
+ }
6721
+ }
6722
+ return null;
6723
+ };
6724
+ for (const param of field(definitionNode, "parameters")?.namedChildren ?? []) {
6725
+ if (param === null) {
6726
+ continue;
6727
+ }
6728
+ const inDefault = field(param, "value");
6729
+ const found = inDefault === null ? findIn(field(param, "type"), 0) : dependencyNameOf(inDefault, form);
6730
+ if (found !== null) {
6731
+ names.push(found);
6732
+ }
6733
+ }
6734
+ return names;
6735
+ }
6736
+ function wrapperUnit(target, declared, options) {
6737
+ const { file, node, name, exportPath } = target;
6738
+ const body = field(node, "body");
6739
+ const range = rangeOf(node);
6740
+ const raised = raisedResponses(body, {
6741
+ calls: declared.pattern.responseStatusCalls ?? [],
6742
+ module: file.module,
6743
+ facts: options.facts
6744
+ });
6745
+ const continuations = continuationStatements(node, declared.form);
6746
+ const terminals = bodyTerminals(body, raised, continuations);
6747
+ const effects = invocationEffects(node);
6748
+ const extra = recognizedBodyEffects(
6749
+ node,
6750
+ file.module,
6751
+ options.storageFor(file)
6752
+ );
6753
+ const delegate = (at) => ({
6754
+ terminal: {
6755
+ kind: "delegate",
6756
+ statusCode: null,
6757
+ body: null,
6758
+ exceptionType: null,
6759
+ message: null,
6760
+ component: null,
6761
+ renderTree: null,
6762
+ delegateTarget: null,
6763
+ emitEvent: null,
6764
+ location: rangeOf(at)
6765
+ },
6766
+ location: rangeOf(at)
6767
+ });
6768
+ const respond = (statement) => returnBranch(statement, {
6769
+ pattern: declared.pattern,
6770
+ responseShape: { reading: absentReading2 },
6771
+ declaredStatus: {
6772
+ reading: absentReading2,
6773
+ ...declared.pattern.defaultStatusCode !== void 0 ? { libraryDefault: declared.pattern.defaultStatusCode } : {}
6774
+ },
6775
+ module: file.module,
6776
+ facts: options.facts
6777
+ });
6778
+ const branchOf = (terminal) => {
6779
+ if (terminal.type === "raise") {
6780
+ return {
6781
+ terminal: terminal.terminal,
6782
+ location: rangeOf(terminal.statement)
6783
+ };
6784
+ }
6785
+ if (terminal.type === "continuation" || returnHandsOn(terminal.statement, declared.form)) {
6786
+ return delegate(terminal.statement);
6787
+ }
6788
+ return respond(terminal.statement);
6789
+ };
6790
+ const branches = enumerateBodyBranches({
6791
+ body,
6792
+ terminals,
6793
+ raised,
6794
+ effects,
6795
+ branchOf,
6796
+ fallthrough: delegate(node)
6797
+ }).map(
6798
+ (branch) => extra.length === 0 ? branch : { ...branch, extraEffects: extra }
6799
+ );
6800
+ return {
6801
+ identity: {
6802
+ name,
6803
+ nameKind: "binding",
6804
+ kind: "middleware",
6805
+ file: file.displayPath,
6806
+ range,
6807
+ span: spanOf(node),
6808
+ exportName: exportPath[0] ?? name,
6809
+ exportPath
6810
+ },
6811
+ boundaryBinding: null,
6812
+ parameters: [],
6813
+ branches,
6814
+ bodyContent: body === null ? "absent" : bodyContentOf(body),
6815
+ dependencyCalls: [],
6816
+ declaredContract: null
6817
+ };
6818
+ }
6819
+ function returnHandsOn(statement, form) {
6820
+ if (form.type === "dependency") {
6821
+ return true;
6822
+ }
6823
+ if (form.continuationParam !== void 0 || form.throwParam !== void 0) {
6824
+ return false;
6825
+ }
6826
+ if (form.returnedValueResponds === true) {
6827
+ const returned = statement.namedChildren[0];
6828
+ return returned === void 0 || returned === null || returned.type === "none";
6829
+ }
6830
+ return true;
6831
+ }
6832
+ function continuationStatements(node, form) {
6833
+ if (form.type !== "decoratedWrapper" || form.continuationParam === void 0) {
6834
+ return [];
6835
+ }
6836
+ const params = (field(node, "parameters")?.namedChildren ?? []).filter(
6837
+ (param2) => param2 !== null
6838
+ );
6839
+ const param = params[form.continuationParam];
6840
+ const continuation = param === void 0 ? null : field(param, "name")?.text ?? (param.type === "identifier" ? param.text : null);
6841
+ if (continuation === null) {
6842
+ return [];
6843
+ }
6844
+ const body = field(node, "body");
6845
+ if (body === null) {
6846
+ return [];
6847
+ }
6848
+ const found = [];
6849
+ for (const call of bodyCalls(body)) {
6850
+ const callee = field(call, "function");
6851
+ const statement = callee?.type === "identifier" && callee.text === continuation ? enclosingStatement(call, body) : null;
6852
+ if (statement !== null) {
6853
+ found.push(statement);
6854
+ }
6855
+ }
6856
+ return found;
6857
+ }
6858
+
6307
6859
  // src/project.ts
6308
6860
  function reportUnresolvedProjectModules(options, db) {
6309
6861
  const imported = new Set(db.facts("pyImport").map((row) => String(row[1])));
@@ -6401,24 +6953,43 @@ async function extractPythonProject(options) {
6401
6953
  couldMatch,
6402
6954
  rawSql: rawSqlPatterns
6403
6955
  } : void 0;
6404
- const seeds = [];
6405
- const summariesBySeed = /* @__PURE__ */ new Map();
6956
+ const wrapperIndex = timer.time(
6957
+ "discover",
6958
+ () => buildWrapperIndex(bound, {
6959
+ packs: options.packs,
6960
+ roots: options.roots,
6961
+ facts: needsValues ? db : void 0,
6962
+ storageFor
6963
+ })
6964
+ );
6965
+ const discovered = /* @__PURE__ */ new Map();
6406
6966
  for (const boundFile of bound) {
6407
6967
  const { file, root, module: moduleBinding } = boundFile;
6408
- const displayPath = displayPathOf(file);
6409
6968
  const storage = storageFor(boundFile);
6410
6969
  const rawUnits = timer.time(
6411
6970
  "discover",
6412
6971
  () => discoverUnits(root, moduleBinding, {
6413
6972
  packs: options.packs,
6414
- filePath: displayPath,
6973
+ filePath: displayPathOf(file),
6415
6974
  absoluteFile: file,
6416
6975
  routerIndex,
6417
6976
  gapHandling,
6977
+ wrappers: wrapperIndex,
6418
6978
  ...needsValues ? { facts: db } : {},
6419
6979
  ...storage === void 0 ? {} : { storage }
6420
6980
  })
6421
6981
  );
6982
+ discovered.set(file, rawUnits);
6983
+ }
6984
+ const seeds = [];
6985
+ const summariesBySeed = /* @__PURE__ */ new Map();
6986
+ for (const boundFile of bound) {
6987
+ const { file, root, module: moduleBinding } = boundFile;
6988
+ const displayPath = displayPathOf(file);
6989
+ const rawUnits = [
6990
+ ...discovered.get(file) ?? [],
6991
+ ...wrapperIndex.unitsIn(file)
6992
+ ];
6422
6993
  for (const raw of rawUnits) {
6423
6994
  const summary = timer.time(
6424
6995
  "summarize",
@@ -6506,12 +7077,16 @@ async function extractPythonProject(options) {
6506
7077
  }
6507
7078
  disambiguateSummaryIds(summaries);
6508
7079
  linkCallsToSummaries(summaries);
7080
+ const composed = timer.time(
7081
+ "summarize",
7082
+ () => composeWrappers(summaries, { gapHandling })
7083
+ );
6509
7084
  await timer.timeAsync("cache.write", async () => {
6510
- if (cacheDir === null || summaries.length === 0) {
7085
+ if (cacheDir === null || composed.length === 0) {
6511
7086
  return;
6512
7087
  }
6513
7088
  try {
6514
- await cache.write(cacheInput, summaries);
7089
+ await cache.write(cacheInput, composed);
6515
7090
  } catch {
6516
7091
  }
6517
7092
  });
@@ -6520,11 +7095,11 @@ async function extractPythonProject(options) {
6520
7095
  packs: options.packs,
6521
7096
  tallies,
6522
7097
  filesWalked: options.files.length,
6523
- summaries
7098
+ summaries: composed
6524
7099
  })
6525
7100
  );
6526
7101
  options.onTiming?.(timer.report());
6527
- return { summaries, facts: db };
7102
+ return { summaries: composed, facts: db };
6528
7103
  }
6529
7104
  function resolvedImportsOf(db, displayPathOf) {
6530
7105
  const importsByFile = /* @__PURE__ */ new Map();