@suss/adapter-python 0.31.1 → 0.32.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/README.md CHANGED
@@ -26,6 +26,7 @@ npx suss check --dir summaries/
26
26
  - FastAPI and flask-restx routes, including a decorator a project re-exports through a wrapper module, with `response_model` and `status_code` taken as the declared contract.
27
27
  - The path a route is served under, composed through `include_router` prefixes, however many mounts deep, including a mount written inside a function or spread out of a dictionary.
28
28
  - SQLAlchemy calls, `requests` / `httpx` / `aiohttp` call sites, what a file reads from the environment, and what a handler calls out to, as facts the checker's rules run over.
29
+ - Environment reads written through a project helper: with `def env(key): return os.environ[key]`, a call to `env("DATABASE_URL")` is a read of `DATABASE_URL`, however many helpers the name is handed through on the way. Not covered: a name built with an f-string (`env(f"{prefix}_URL")`), a name the helper takes off a dict, and a callee that is a value rather than a function the project declares (`env = make_reader()`).
29
30
 
30
31
  How each of those is decided, and where it stops: [how the Python adapter reads a project](./DESIGN.md).
31
32
 
package/dist/index.cjs CHANGED
@@ -784,6 +784,9 @@ var import_resolution = require("@suss/resolution");
784
784
  function resolveCalls(db, callKeys) {
785
785
  (0, import_resolution.askResolution)(db, callKeys);
786
786
  }
787
+ function resolveEnvSites(db, siteIds) {
788
+ (0, import_resolution.askResolution)(db, siteIds, "wantedEnvSite");
789
+ }
787
790
  function writtenValueUnder(db, key, site) {
788
791
  if ((0, import_resolution.askResolutionUnder)(db, [[key, site]]) === "abandoned") {
789
792
  return null;
@@ -992,31 +995,46 @@ function emitCall(emitter, call) {
992
995
  if (!emitter.insideMethod) {
993
996
  add(emitter, "callOutsideMethod", callKey);
994
997
  }
998
+ for (const argument of callArguments(call)) {
999
+ if (argument.kind === "keyword") {
1000
+ add(
1001
+ emitter,
1002
+ "callKeywordArg",
1003
+ callKey,
1004
+ argument.name,
1005
+ valueKey(emitter, argument.node)
1006
+ );
1007
+ continue;
1008
+ }
1009
+ add(
1010
+ emitter,
1011
+ "callArg",
1012
+ callKey,
1013
+ String(argument.position),
1014
+ valueKey(emitter, argument.node)
1015
+ );
1016
+ }
1017
+ }
1018
+ function callArguments(call) {
1019
+ const args = field(call, "arguments");
1020
+ if (args === null) {
1021
+ return [];
1022
+ }
1023
+ const written = [];
995
1024
  let position = 0;
996
1025
  for (const argument of children(args)) {
997
1026
  if (argument.type === "keyword_argument") {
998
1027
  const name = field(argument, "name");
999
1028
  const value = field(argument, "value");
1000
1029
  if (name !== null && value !== null) {
1001
- add(
1002
- emitter,
1003
- "callKeywordArg",
1004
- callKey,
1005
- name.text,
1006
- valueKey(emitter, value)
1007
- );
1030
+ written.push({ kind: "keyword", name: name.text, node: value });
1008
1031
  }
1009
1032
  continue;
1010
1033
  }
1011
- add(
1012
- emitter,
1013
- "callArg",
1014
- callKey,
1015
- String(position),
1016
- valueKey(emitter, argument)
1017
- );
1034
+ written.push({ kind: "positional", position, node: argument });
1018
1035
  position += 1;
1019
1036
  }
1037
+ return written;
1020
1038
  }
1021
1039
  function valueKey(emitter, value) {
1022
1040
  if (value.type !== "identifier") {
@@ -2511,6 +2529,10 @@ function writtenNodeOf(node, db) {
2511
2529
  const bound = db === void 0 ? void 0 : projects.get(db);
2512
2530
  return bound?.context.writtenTo(node) ?? null;
2513
2531
  }
2532
+ function resolutionKeyOf(node, db) {
2533
+ const bound = db === void 0 ? void 0 : projects.get(db);
2534
+ return bound?.keyOf(node) ?? null;
2535
+ }
2514
2536
  var NOTHING_BUILT = { type: "noCall" };
2515
2537
  function constructionBehind(node, db) {
2516
2538
  if (db === void 0) {
@@ -2857,7 +2879,7 @@ function passesParametersThrough(def, inner) {
2857
2879
  // src/discovery.ts
2858
2880
  var import_behavioral_ir4 = require("@suss/behavioral-ir");
2859
2881
  var import_extractor5 = require("@suss/extractor");
2860
- var import_values10 = require("@suss/values");
2882
+ var import_values11 = require("@suss/values");
2861
2883
 
2862
2884
  // src/clientCalls.ts
2863
2885
  var import_behavioral_ir2 = require("@suss/behavioral-ir");
@@ -3575,14 +3597,22 @@ var import_behavioral_ir3 = require("@suss/behavioral-ir");
3575
3597
  var import_extractor4 = require("@suss/extractor");
3576
3598
  var PYTHON_ENV_RECOGNITION = "python-env";
3577
3599
  var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
3578
- function envReadEffects(root, module2) {
3579
- const effects = [];
3600
+ function envReadEffects(root, module2, facts) {
3601
+ const slots = [];
3602
+ const readsHelpers = facts !== void 0 && sitesByDb.has(facts);
3580
3603
  const startScope = module2.scopeFor.get(root.id) ?? module2.moduleScope;
3581
3604
  (0, import_extractor4.walkDescendants)(root, startScope, {
3582
3605
  at: (node, scope) => {
3583
- const read = envReadAt(node, scope);
3584
- if (read !== null) {
3585
- effects.push(configReadEffect(read));
3606
+ const syntax = envReadSyntaxAt(node, scope);
3607
+ if (syntax !== null) {
3608
+ const name = stringLiteralValue(syntax.name);
3609
+ if (name !== null) {
3610
+ slots.push({ read: { name, defaulted: syntax.defaulted } });
3611
+ }
3612
+ return;
3613
+ }
3614
+ if (readsHelpers && node.type === "call") {
3615
+ slots.push({ call: node });
3586
3616
  }
3587
3617
  },
3588
3618
  into: (node, scope) => {
@@ -3592,7 +3622,16 @@ function envReadEffects(root, module2) {
3592
3622
  return module2.scopeFor.get(node.id) ?? scope;
3593
3623
  }
3594
3624
  });
3595
- return effects;
3625
+ const throughHelpers = helperReadsByCall(
3626
+ slots.flatMap((slot) => "call" in slot ? [slot.call] : []),
3627
+ facts
3628
+ );
3629
+ return slots.flatMap((slot) => {
3630
+ if ("read" in slot) {
3631
+ return [configReadEffect(slot.read)];
3632
+ }
3633
+ return throughHelpers.get(slot.call.id) ?? [];
3634
+ });
3596
3635
  }
3597
3636
  function configReadEffect(read) {
3598
3637
  return {
@@ -3610,7 +3649,7 @@ function configReadEffect(read) {
3610
3649
  }
3611
3650
  };
3612
3651
  }
3613
- function envReadAt(node, scope) {
3652
+ function envReadSyntaxAt(node, scope) {
3614
3653
  if (node.type === "subscript") {
3615
3654
  return subscriptRead(node, scope);
3616
3655
  }
@@ -3625,11 +3664,10 @@ function subscriptRead(node, scope) {
3625
3664
  if (value === null || index === null || !isEnviron(value, scope)) {
3626
3665
  return null;
3627
3666
  }
3628
- const name = stringLiteralValue(index);
3629
- if (name === null || isAssignedTo(node)) {
3667
+ if (isAssignedTo(node)) {
3630
3668
  return null;
3631
3669
  }
3632
- return { name, defaulted: isDefaultedAt(node) };
3670
+ return { name: index, defaulted: isDefaultedAt(node) };
3633
3671
  }
3634
3672
  function isAssignedTo(node) {
3635
3673
  const parent = node.parent;
@@ -3643,22 +3681,20 @@ function isAssignedTo(node) {
3643
3681
  }
3644
3682
  function callRead(node, scope) {
3645
3683
  const callee = field(node, "function");
3646
- const args = field(node, "arguments");
3647
- if (callee === null || args === null || !isEnvGetter(callee, scope)) {
3684
+ if (callee === null || !isEnvGetter(callee, scope)) {
3648
3685
  return null;
3649
3686
  }
3650
- const positional = args.namedChildren.filter(
3651
- (arg) => arg !== null && arg.type !== "keyword_argument"
3687
+ const written = callArguments(node);
3688
+ const name = written.find(
3689
+ (argument) => argument.kind === "positional" && argument.position === 0
3652
3690
  );
3653
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
3654
- if (name === null) {
3691
+ if (name === void 0) {
3655
3692
  return null;
3656
3693
  }
3657
- const keywords = args.namedChildren.filter(
3658
- (arg) => arg !== null && arg.type === "keyword_argument"
3694
+ const hasDefault = written.some(
3695
+ (argument) => argument.kind === "positional" && argument.position === 1 || argument.kind === "keyword" && argument.name === "default"
3659
3696
  );
3660
- const hasDefault = positional.length > 1 || keywords.some((arg) => field(arg, "name")?.text === "default");
3661
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
3697
+ return { name: name.node, defaulted: hasDefault || isDefaultedAt(node) };
3662
3698
  }
3663
3699
  function isEnvGetter(callee, scope) {
3664
3700
  if (callee.type === "attribute") {
@@ -3715,9 +3751,153 @@ function isDefaultedAt(node) {
3715
3751
  }
3716
3752
  return false;
3717
3753
  }
3754
+ function envNameSites(filePath, root, module2) {
3755
+ const sites = [];
3756
+ (0, import_extractor4.walkDescendants)(root, module2.moduleScope, {
3757
+ at: (node, scope) => {
3758
+ const syntax = envReadSyntaxAt(node, scope);
3759
+ if (syntax === null || stringLiteralValue(syntax.name) !== null) {
3760
+ return;
3761
+ }
3762
+ sites.push({
3763
+ site: nodeId(filePath, node),
3764
+ nameKey: readKey(filePath, syntax.name, enclosingFunction(syntax.name)),
3765
+ defaulted: syntax.defaulted
3766
+ });
3767
+ },
3768
+ into: (node, scope) => module2.scopeFor.get(node.id) ?? scope
3769
+ });
3770
+ return sites;
3771
+ }
3772
+ var sitesByDb = /* @__PURE__ */ new WeakMap();
3773
+ function bindEnvNameSites(db, sites) {
3774
+ if (sites.length === 0) {
3775
+ return;
3776
+ }
3777
+ let index = sitesByDb.get(db);
3778
+ if (index === void 0) {
3779
+ index = { byId: /* @__PURE__ */ new Map(), sitesByParameter: null };
3780
+ sitesByDb.set(db, index);
3781
+ }
3782
+ for (const site of sites) {
3783
+ db.add("readsEnvNamed", [site.site, site.nameKey]);
3784
+ index.byId.set(site.site, site);
3785
+ }
3786
+ }
3787
+ function sitesEachParameterNames(db, index) {
3788
+ if (index.sitesByParameter !== null) {
3789
+ return index.sitesByParameter;
3790
+ }
3791
+ resolveEnvSites(db, [...index.byId.keys()]);
3792
+ const found = /* @__PURE__ */ new Map();
3793
+ for (const row of db.facts("wantedParamNamesEnv")) {
3794
+ const parameter = String(row[0]);
3795
+ const named = found.get(parameter) ?? [];
3796
+ named.push(String(row[1]));
3797
+ found.set(parameter, named);
3798
+ }
3799
+ index.sitesByParameter = found;
3800
+ return found;
3801
+ }
3802
+ function helperReadsByCall(calls, db) {
3803
+ const found = /* @__PURE__ */ new Map();
3804
+ const index = db === void 0 ? void 0 : sitesByDb.get(db);
3805
+ if (db === void 0 || index === void 0 || calls.length === 0) {
3806
+ return found;
3807
+ }
3808
+ const named = sitesEachParameterNames(db, index);
3809
+ if (named.size === 0) {
3810
+ return found;
3811
+ }
3812
+ const calleeKeys = /* @__PURE__ */ new Map();
3813
+ for (const call of calls) {
3814
+ const callee = field(call, "function");
3815
+ const key = callee === null ? null : resolutionKeyOf(callee, db);
3816
+ if (key !== null) {
3817
+ calleeKeys.set(call.id, key);
3818
+ }
3819
+ }
3820
+ if (calleeKeys.size === 0) {
3821
+ return found;
3822
+ }
3823
+ resolveCalls(db, [...new Set(calleeKeys.values())]);
3824
+ for (const call of calls) {
3825
+ const key = calleeKeys.get(call.id);
3826
+ if (key === void 0) {
3827
+ continue;
3828
+ }
3829
+ const argumentAt2 = argumentsByParameter(db, call, key);
3830
+ if (argumentAt2.size === 0) {
3831
+ continue;
3832
+ }
3833
+ const reads = readsThroughHelper(db, call, argumentAt2, index.byId, named);
3834
+ if (reads.length > 0) {
3835
+ found.set(call.id, reads);
3836
+ }
3837
+ }
3838
+ return found;
3839
+ }
3840
+ function argumentsByParameter(db, call, calleeKey) {
3841
+ const written = callArguments(call);
3842
+ if (written.length === 0) {
3843
+ return /* @__PURE__ */ new Map();
3844
+ }
3845
+ const positional = /* @__PURE__ */ new Map();
3846
+ const byName = /* @__PURE__ */ new Map();
3847
+ for (const argument of written) {
3848
+ if (argument.kind === "keyword") {
3849
+ byName.set(argument.name, argument.node);
3850
+ continue;
3851
+ }
3852
+ positional.set(String(argument.position), argument.node);
3853
+ }
3854
+ const argumentAt2 = /* @__PURE__ */ new Map();
3855
+ for (const resolved of db.lookup("wantedResolves", 0, calleeKey)) {
3856
+ const callee = String(resolved[1]);
3857
+ for (const row of db.lookup("paramOf", 0, callee)) {
3858
+ const at = positional.get(String(row[1]));
3859
+ if (at !== void 0) {
3860
+ argumentAt2.set(String(row[2]), at);
3861
+ }
3862
+ }
3863
+ for (const row of db.lookup("paramNamed", 0, callee)) {
3864
+ const at = byName.get(String(row[1]));
3865
+ if (at !== void 0) {
3866
+ argumentAt2.set(String(row[2]), at);
3867
+ }
3868
+ }
3869
+ }
3870
+ return argumentAt2;
3871
+ }
3872
+ function readsThroughHelper(db, call, argumentAt2, siteById, named) {
3873
+ const reads = /* @__PURE__ */ new Map();
3874
+ for (const [parameter, argument] of argumentAt2) {
3875
+ const sites = named.get(parameter);
3876
+ if (sites === void 0) {
3877
+ continue;
3878
+ }
3879
+ const name = stringLiteralValue(argument) ?? stringValueOf(argument, db);
3880
+ if (name === null) {
3881
+ continue;
3882
+ }
3883
+ for (const id of sites) {
3884
+ const site = siteById.get(id);
3885
+ if (site !== void 0) {
3886
+ reads.set(name, (reads.get(name) ?? true) && site.defaulted);
3887
+ }
3888
+ }
3889
+ }
3890
+ if (reads.size === 0) {
3891
+ return [];
3892
+ }
3893
+ const defaultedAtCall = isDefaultedAt(call);
3894
+ return [...reads].map(
3895
+ ([name, defaulted]) => configReadEffect({ name, defaulted: defaulted || defaultedAtCall })
3896
+ );
3897
+ }
3718
3898
 
3719
3899
  // src/paths/raisedResponses.ts
3720
- var import_values8 = require("@suss/values");
3900
+ var import_values9 = require("@suss/values");
3721
3901
  var NESTED_DEFINITION_TYPES2 = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
3722
3902
  function raisedResponses(body, options) {
3723
3903
  if (body === null || options.calls.length === 0) {
@@ -3811,7 +3991,7 @@ function statusOf(declared, call, facts) {
3811
3991
  if (written === null) {
3812
3992
  return declared.defaultStatusCode === void 0 ? null : { type: "literal", value: declared.defaultStatusCode };
3813
3993
  }
3814
- const value = (0, import_values8.constantOf)(evaluatedValue(written, facts));
3994
+ const value = (0, import_values9.constantOf)(evaluatedValue(written, facts));
3815
3995
  if (typeof value === "number") {
3816
3996
  return { type: "literal", value };
3817
3997
  }
@@ -4584,7 +4764,7 @@ function readPathArgument(classification, options) {
4584
4764
  const first = classification.args[0];
4585
4765
  if (first !== void 0) {
4586
4766
  const value = evaluatedValue(first.node, options.facts);
4587
- const path6 = (0, import_values10.literalOf)(value) ?? (0, import_values10.pathOf)(value);
4767
+ const path6 = (0, import_values11.literalOf)(value) ?? (0, import_values11.pathOf)(value);
4588
4768
  if (path6 !== void 0) {
4589
4769
  return (0, import_extractor5.writtenReading)(path6, classification.range);
4590
4770
  }
@@ -4831,11 +5011,11 @@ function statusFromReturnedValue(node, facts) {
4831
5011
  return null;
4832
5012
  }
4833
5013
  const value = evaluatedValue(node, facts);
4834
- const number = (0, import_values10.constantOf)(value);
5014
+ const number = (0, import_values11.constantOf)(value);
4835
5015
  if (typeof number === "number") {
4836
5016
  return Number.isInteger(number) ? number : null;
4837
5017
  }
4838
- const text2 = (0, import_values10.literalOf)(value);
5018
+ const text2 = (0, import_values11.literalOf)(value);
4839
5019
  if (text2 === null) {
4840
5020
  return null;
4841
5021
  }
@@ -5178,7 +5358,12 @@ function buildRouteUnit(options) {
5178
5358
  () => readReturnAnnotation(definitionNode, enclosingScope, ctx)
5179
5359
  );
5180
5360
  const effects = invocationEffects(definitionNode, options.facts);
5181
- const extra = recognizedBodyEffects(definitionNode, module2, storageLookup);
5361
+ const extra = recognizedBodyEffects(
5362
+ definitionNode,
5363
+ module2,
5364
+ storageLookup,
5365
+ options.facts
5366
+ );
5182
5367
  const perTerminal = branchesPerTerminal({
5183
5368
  definitionNode,
5184
5369
  effects,
@@ -5263,7 +5448,7 @@ function unreadRoleReadings(parameters, range) {
5263
5448
  )
5264
5449
  ];
5265
5450
  }
5266
- function recognizedBodyEffects(definitionNode, module2, storageLookup) {
5451
+ function recognizedBodyEffects(definitionNode, module2, storageLookup, facts) {
5267
5452
  const storage = storageLookup === void 0 ? [] : [
5268
5453
  ...storageEffects(bodyCalls(definitionNode), {
5269
5454
  ...storageLookup,
@@ -5275,7 +5460,7 @@ function recognizedBodyEffects(definitionNode, module2, storageLookup) {
5275
5460
  patterns: storageLookup.rawSql ?? []
5276
5461
  })
5277
5462
  ];
5278
- return [...envReadEffects(definitionNode, module2), ...storage];
5463
+ return [...envReadEffects(definitionNode, module2, facts), ...storage];
5279
5464
  }
5280
5465
  function recognizedCallIds(definitionNode, storageLookup) {
5281
5466
  if (storageLookup === void 0) {
@@ -6168,7 +6353,8 @@ function libraryUnit(target, options) {
6168
6353
  const extra = recognizedBodyEffects(
6169
6354
  node,
6170
6355
  file.module,
6171
- options.storageFor(file)
6356
+ options.storageFor(file),
6357
+ options.facts
6172
6358
  );
6173
6359
  const range = rangeOf(node);
6174
6360
  return {
@@ -7749,7 +7935,8 @@ function wrapperUnit(target, declared, options) {
7749
7935
  const extra = recognizedBodyEffects(
7750
7936
  node,
7751
7937
  file.module,
7752
- options.storageFor(file)
7938
+ options.storageFor(file),
7939
+ options.facts
7753
7940
  );
7754
7941
  const delegate = (at) => ({
7755
7942
  terminal: {
@@ -7907,6 +8094,10 @@ function factsForFile(options) {
7907
8094
  files: [{ file: options.file, root: options.root, module: options.module }],
7908
8095
  definitions
7909
8096
  });
8097
+ bindEnvNameSites(
8098
+ db,
8099
+ envNameSites(options.file, options.root, options.module)
8100
+ );
7910
8101
  (0, import_resolution7.addPackWords)(db, packWordsOf(options.packs));
7911
8102
  return db;
7912
8103
  }
@@ -7964,7 +8155,10 @@ async function extractPythonProject(options) {
7964
8155
  (client) => (client.receiverConstructors ?? []).length > 0
7965
8156
  )
7966
8157
  );
7967
- const needsValues = discovers || mountsRouters || buildsReceivers || storagePatterns.length > 0 || modelQueries.length > 0;
8158
+ const envSites = bound.flatMap(
8159
+ (boundFile) => envNameSites(boundFile.file, boundFile.root, boundFile.module)
8160
+ );
8161
+ const needsValues = discovers || mountsRouters || buildsReceivers || envSites.length > 0 || storagePatterns.length > 0 || modelQueries.length > 0;
7968
8162
  const definitions = /* @__PURE__ */ new Map();
7969
8163
  timer.time("discover", () => {
7970
8164
  for (const { file, root, module: moduleBinding } of bound) {
@@ -7976,6 +8170,7 @@ async function extractPythonProject(options) {
7976
8170
  }
7977
8171
  if (needsValues) {
7978
8172
  bindEvaluator(db, { files: bound, definitions });
8173
+ bindEnvNameSites(db, envSites);
7979
8174
  }
7980
8175
  (0, import_resolution7.addPackWords)(db, packWordsOf(options.packs));
7981
8176
  });
@@ -8064,7 +8259,7 @@ async function extractPythonProject(options) {
8064
8259
  }
8065
8260
  const loadTimeReads = timer.time(
8066
8261
  "discover",
8067
- () => envReadEffects(root, moduleBinding)
8262
+ () => envReadEffects(root, moduleBinding, db)
8068
8263
  );
8069
8264
  if (loadTimeReads.length > 0) {
8070
8265
  const summary = timer.time(
package/dist/index.js CHANGED
@@ -724,6 +724,9 @@ import {
724
724
  function resolveCalls(db, callKeys) {
725
725
  askResolution(db, callKeys);
726
726
  }
727
+ function resolveEnvSites(db, siteIds) {
728
+ askResolution(db, siteIds, "wantedEnvSite");
729
+ }
727
730
  function writtenValueUnder(db, key, site) {
728
731
  if (askResolutionUnder(db, [[key, site]]) === "abandoned") {
729
732
  return null;
@@ -936,31 +939,46 @@ function emitCall(emitter, call) {
936
939
  if (!emitter.insideMethod) {
937
940
  add(emitter, "callOutsideMethod", callKey);
938
941
  }
942
+ for (const argument of callArguments(call)) {
943
+ if (argument.kind === "keyword") {
944
+ add(
945
+ emitter,
946
+ "callKeywordArg",
947
+ callKey,
948
+ argument.name,
949
+ valueKey(emitter, argument.node)
950
+ );
951
+ continue;
952
+ }
953
+ add(
954
+ emitter,
955
+ "callArg",
956
+ callKey,
957
+ String(argument.position),
958
+ valueKey(emitter, argument.node)
959
+ );
960
+ }
961
+ }
962
+ function callArguments(call) {
963
+ const args = field(call, "arguments");
964
+ if (args === null) {
965
+ return [];
966
+ }
967
+ const written = [];
939
968
  let position = 0;
940
969
  for (const argument of children(args)) {
941
970
  if (argument.type === "keyword_argument") {
942
971
  const name = field(argument, "name");
943
972
  const value = field(argument, "value");
944
973
  if (name !== null && value !== null) {
945
- add(
946
- emitter,
947
- "callKeywordArg",
948
- callKey,
949
- name.text,
950
- valueKey(emitter, value)
951
- );
974
+ written.push({ kind: "keyword", name: name.text, node: value });
952
975
  }
953
976
  continue;
954
977
  }
955
- add(
956
- emitter,
957
- "callArg",
958
- callKey,
959
- String(position),
960
- valueKey(emitter, argument)
961
- );
978
+ written.push({ kind: "positional", position, node: argument });
962
979
  position += 1;
963
980
  }
981
+ return written;
964
982
  }
965
983
  function valueKey(emitter, value) {
966
984
  if (value.type !== "identifier") {
@@ -2476,6 +2494,10 @@ function writtenNodeOf(node, db) {
2476
2494
  const bound = db === void 0 ? void 0 : projects.get(db);
2477
2495
  return bound?.context.writtenTo(node) ?? null;
2478
2496
  }
2497
+ function resolutionKeyOf(node, db) {
2498
+ const bound = db === void 0 ? void 0 : projects.get(db);
2499
+ return bound?.keyOf(node) ?? null;
2500
+ }
2479
2501
  var NOTHING_BUILT = { type: "noCall" };
2480
2502
  function constructionBehind(node, db) {
2481
2503
  if (db === void 0) {
@@ -3550,14 +3572,22 @@ import { runtimeConfigBinding } from "@suss/behavioral-ir";
3550
3572
  import { SKIP_CHILDREN, walkDescendants } from "@suss/extractor";
3551
3573
  var PYTHON_ENV_RECOGNITION = "python-env";
3552
3574
  var DEFERRED_BODY_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda"]);
3553
- function envReadEffects(root, module) {
3554
- const effects = [];
3575
+ function envReadEffects(root, module, facts) {
3576
+ const slots = [];
3577
+ const readsHelpers = facts !== void 0 && sitesByDb.has(facts);
3555
3578
  const startScope = module.scopeFor.get(root.id) ?? module.moduleScope;
3556
3579
  walkDescendants(root, startScope, {
3557
3580
  at: (node, scope) => {
3558
- const read = envReadAt(node, scope);
3559
- if (read !== null) {
3560
- effects.push(configReadEffect(read));
3581
+ const syntax = envReadSyntaxAt(node, scope);
3582
+ if (syntax !== null) {
3583
+ const name = stringLiteralValue(syntax.name);
3584
+ if (name !== null) {
3585
+ slots.push({ read: { name, defaulted: syntax.defaulted } });
3586
+ }
3587
+ return;
3588
+ }
3589
+ if (readsHelpers && node.type === "call") {
3590
+ slots.push({ call: node });
3561
3591
  }
3562
3592
  },
3563
3593
  into: (node, scope) => {
@@ -3567,7 +3597,16 @@ function envReadEffects(root, module) {
3567
3597
  return module.scopeFor.get(node.id) ?? scope;
3568
3598
  }
3569
3599
  });
3570
- return effects;
3600
+ const throughHelpers = helperReadsByCall(
3601
+ slots.flatMap((slot) => "call" in slot ? [slot.call] : []),
3602
+ facts
3603
+ );
3604
+ return slots.flatMap((slot) => {
3605
+ if ("read" in slot) {
3606
+ return [configReadEffect(slot.read)];
3607
+ }
3608
+ return throughHelpers.get(slot.call.id) ?? [];
3609
+ });
3571
3610
  }
3572
3611
  function configReadEffect(read) {
3573
3612
  return {
@@ -3585,7 +3624,7 @@ function configReadEffect(read) {
3585
3624
  }
3586
3625
  };
3587
3626
  }
3588
- function envReadAt(node, scope) {
3627
+ function envReadSyntaxAt(node, scope) {
3589
3628
  if (node.type === "subscript") {
3590
3629
  return subscriptRead(node, scope);
3591
3630
  }
@@ -3600,11 +3639,10 @@ function subscriptRead(node, scope) {
3600
3639
  if (value === null || index === null || !isEnviron(value, scope)) {
3601
3640
  return null;
3602
3641
  }
3603
- const name = stringLiteralValue(index);
3604
- if (name === null || isAssignedTo(node)) {
3642
+ if (isAssignedTo(node)) {
3605
3643
  return null;
3606
3644
  }
3607
- return { name, defaulted: isDefaultedAt(node) };
3645
+ return { name: index, defaulted: isDefaultedAt(node) };
3608
3646
  }
3609
3647
  function isAssignedTo(node) {
3610
3648
  const parent = node.parent;
@@ -3618,22 +3656,20 @@ function isAssignedTo(node) {
3618
3656
  }
3619
3657
  function callRead(node, scope) {
3620
3658
  const callee = field(node, "function");
3621
- const args = field(node, "arguments");
3622
- if (callee === null || args === null || !isEnvGetter(callee, scope)) {
3659
+ if (callee === null || !isEnvGetter(callee, scope)) {
3623
3660
  return null;
3624
3661
  }
3625
- const positional = args.namedChildren.filter(
3626
- (arg) => arg !== null && arg.type !== "keyword_argument"
3662
+ const written = callArguments(node);
3663
+ const name = written.find(
3664
+ (argument) => argument.kind === "positional" && argument.position === 0
3627
3665
  );
3628
- const name = positional[0] === void 0 ? null : stringLiteralValue(positional[0]);
3629
- if (name === null) {
3666
+ if (name === void 0) {
3630
3667
  return null;
3631
3668
  }
3632
- const keywords = args.namedChildren.filter(
3633
- (arg) => arg !== null && arg.type === "keyword_argument"
3669
+ const hasDefault = written.some(
3670
+ (argument) => argument.kind === "positional" && argument.position === 1 || argument.kind === "keyword" && argument.name === "default"
3634
3671
  );
3635
- const hasDefault = positional.length > 1 || keywords.some((arg) => field(arg, "name")?.text === "default");
3636
- return { name, defaulted: hasDefault || isDefaultedAt(node) };
3672
+ return { name: name.node, defaulted: hasDefault || isDefaultedAt(node) };
3637
3673
  }
3638
3674
  function isEnvGetter(callee, scope) {
3639
3675
  if (callee.type === "attribute") {
@@ -3690,6 +3726,150 @@ function isDefaultedAt(node) {
3690
3726
  }
3691
3727
  return false;
3692
3728
  }
3729
+ function envNameSites(filePath, root, module) {
3730
+ const sites = [];
3731
+ walkDescendants(root, module.moduleScope, {
3732
+ at: (node, scope) => {
3733
+ const syntax = envReadSyntaxAt(node, scope);
3734
+ if (syntax === null || stringLiteralValue(syntax.name) !== null) {
3735
+ return;
3736
+ }
3737
+ sites.push({
3738
+ site: nodeId(filePath, node),
3739
+ nameKey: readKey(filePath, syntax.name, enclosingFunction(syntax.name)),
3740
+ defaulted: syntax.defaulted
3741
+ });
3742
+ },
3743
+ into: (node, scope) => module.scopeFor.get(node.id) ?? scope
3744
+ });
3745
+ return sites;
3746
+ }
3747
+ var sitesByDb = /* @__PURE__ */ new WeakMap();
3748
+ function bindEnvNameSites(db, sites) {
3749
+ if (sites.length === 0) {
3750
+ return;
3751
+ }
3752
+ let index = sitesByDb.get(db);
3753
+ if (index === void 0) {
3754
+ index = { byId: /* @__PURE__ */ new Map(), sitesByParameter: null };
3755
+ sitesByDb.set(db, index);
3756
+ }
3757
+ for (const site of sites) {
3758
+ db.add("readsEnvNamed", [site.site, site.nameKey]);
3759
+ index.byId.set(site.site, site);
3760
+ }
3761
+ }
3762
+ function sitesEachParameterNames(db, index) {
3763
+ if (index.sitesByParameter !== null) {
3764
+ return index.sitesByParameter;
3765
+ }
3766
+ resolveEnvSites(db, [...index.byId.keys()]);
3767
+ const found = /* @__PURE__ */ new Map();
3768
+ for (const row of db.facts("wantedParamNamesEnv")) {
3769
+ const parameter = String(row[0]);
3770
+ const named = found.get(parameter) ?? [];
3771
+ named.push(String(row[1]));
3772
+ found.set(parameter, named);
3773
+ }
3774
+ index.sitesByParameter = found;
3775
+ return found;
3776
+ }
3777
+ function helperReadsByCall(calls, db) {
3778
+ const found = /* @__PURE__ */ new Map();
3779
+ const index = db === void 0 ? void 0 : sitesByDb.get(db);
3780
+ if (db === void 0 || index === void 0 || calls.length === 0) {
3781
+ return found;
3782
+ }
3783
+ const named = sitesEachParameterNames(db, index);
3784
+ if (named.size === 0) {
3785
+ return found;
3786
+ }
3787
+ const calleeKeys = /* @__PURE__ */ new Map();
3788
+ for (const call of calls) {
3789
+ const callee = field(call, "function");
3790
+ const key = callee === null ? null : resolutionKeyOf(callee, db);
3791
+ if (key !== null) {
3792
+ calleeKeys.set(call.id, key);
3793
+ }
3794
+ }
3795
+ if (calleeKeys.size === 0) {
3796
+ return found;
3797
+ }
3798
+ resolveCalls(db, [...new Set(calleeKeys.values())]);
3799
+ for (const call of calls) {
3800
+ const key = calleeKeys.get(call.id);
3801
+ if (key === void 0) {
3802
+ continue;
3803
+ }
3804
+ const argumentAt2 = argumentsByParameter(db, call, key);
3805
+ if (argumentAt2.size === 0) {
3806
+ continue;
3807
+ }
3808
+ const reads = readsThroughHelper(db, call, argumentAt2, index.byId, named);
3809
+ if (reads.length > 0) {
3810
+ found.set(call.id, reads);
3811
+ }
3812
+ }
3813
+ return found;
3814
+ }
3815
+ function argumentsByParameter(db, call, calleeKey) {
3816
+ const written = callArguments(call);
3817
+ if (written.length === 0) {
3818
+ return /* @__PURE__ */ new Map();
3819
+ }
3820
+ const positional = /* @__PURE__ */ new Map();
3821
+ const byName = /* @__PURE__ */ new Map();
3822
+ for (const argument of written) {
3823
+ if (argument.kind === "keyword") {
3824
+ byName.set(argument.name, argument.node);
3825
+ continue;
3826
+ }
3827
+ positional.set(String(argument.position), argument.node);
3828
+ }
3829
+ const argumentAt2 = /* @__PURE__ */ new Map();
3830
+ for (const resolved of db.lookup("wantedResolves", 0, calleeKey)) {
3831
+ const callee = String(resolved[1]);
3832
+ for (const row of db.lookup("paramOf", 0, callee)) {
3833
+ const at = positional.get(String(row[1]));
3834
+ if (at !== void 0) {
3835
+ argumentAt2.set(String(row[2]), at);
3836
+ }
3837
+ }
3838
+ for (const row of db.lookup("paramNamed", 0, callee)) {
3839
+ const at = byName.get(String(row[1]));
3840
+ if (at !== void 0) {
3841
+ argumentAt2.set(String(row[2]), at);
3842
+ }
3843
+ }
3844
+ }
3845
+ return argumentAt2;
3846
+ }
3847
+ function readsThroughHelper(db, call, argumentAt2, siteById, named) {
3848
+ const reads = /* @__PURE__ */ new Map();
3849
+ for (const [parameter, argument] of argumentAt2) {
3850
+ const sites = named.get(parameter);
3851
+ if (sites === void 0) {
3852
+ continue;
3853
+ }
3854
+ const name = stringLiteralValue(argument) ?? stringValueOf(argument, db);
3855
+ if (name === null) {
3856
+ continue;
3857
+ }
3858
+ for (const id of sites) {
3859
+ const site = siteById.get(id);
3860
+ if (site !== void 0) {
3861
+ reads.set(name, (reads.get(name) ?? true) && site.defaulted);
3862
+ }
3863
+ }
3864
+ }
3865
+ if (reads.size === 0) {
3866
+ return [];
3867
+ }
3868
+ const defaultedAtCall = isDefaultedAt(call);
3869
+ return [...reads].map(
3870
+ ([name, defaulted]) => configReadEffect({ name, defaulted: defaulted || defaultedAtCall })
3871
+ );
3872
+ }
3693
3873
 
3694
3874
  // src/paths/raisedResponses.ts
3695
3875
  import { constantOf as constantOf3 } from "@suss/values";
@@ -5153,7 +5333,12 @@ function buildRouteUnit(options) {
5153
5333
  () => readReturnAnnotation(definitionNode, enclosingScope, ctx)
5154
5334
  );
5155
5335
  const effects = invocationEffects(definitionNode, options.facts);
5156
- const extra = recognizedBodyEffects(definitionNode, module, storageLookup);
5336
+ const extra = recognizedBodyEffects(
5337
+ definitionNode,
5338
+ module,
5339
+ storageLookup,
5340
+ options.facts
5341
+ );
5157
5342
  const perTerminal = branchesPerTerminal({
5158
5343
  definitionNode,
5159
5344
  effects,
@@ -5238,7 +5423,7 @@ function unreadRoleReadings(parameters, range) {
5238
5423
  )
5239
5424
  ];
5240
5425
  }
5241
- function recognizedBodyEffects(definitionNode, module, storageLookup) {
5426
+ function recognizedBodyEffects(definitionNode, module, storageLookup, facts) {
5242
5427
  const storage = storageLookup === void 0 ? [] : [
5243
5428
  ...storageEffects(bodyCalls(definitionNode), {
5244
5429
  ...storageLookup,
@@ -5250,7 +5435,7 @@ function recognizedBodyEffects(definitionNode, module, storageLookup) {
5250
5435
  patterns: storageLookup.rawSql ?? []
5251
5436
  })
5252
5437
  ];
5253
- return [...envReadEffects(definitionNode, module), ...storage];
5438
+ return [...envReadEffects(definitionNode, module, facts), ...storage];
5254
5439
  }
5255
5440
  function recognizedCallIds(definitionNode, storageLookup) {
5256
5441
  if (storageLookup === void 0) {
@@ -6182,7 +6367,8 @@ function libraryUnit(target, options) {
6182
6367
  const extra = recognizedBodyEffects(
6183
6368
  node,
6184
6369
  file.module,
6185
- options.storageFor(file)
6370
+ options.storageFor(file),
6371
+ options.facts
6186
6372
  );
6187
6373
  const range = rangeOf(node);
6188
6374
  return {
@@ -7763,7 +7949,8 @@ function wrapperUnit(target, declared, options) {
7763
7949
  const extra = recognizedBodyEffects(
7764
7950
  node,
7765
7951
  file.module,
7766
- options.storageFor(file)
7952
+ options.storageFor(file),
7953
+ options.facts
7767
7954
  );
7768
7955
  const delegate = (at) => ({
7769
7956
  terminal: {
@@ -7921,6 +8108,10 @@ function factsForFile(options) {
7921
8108
  files: [{ file: options.file, root: options.root, module: options.module }],
7922
8109
  definitions
7923
8110
  });
8111
+ bindEnvNameSites(
8112
+ db,
8113
+ envNameSites(options.file, options.root, options.module)
8114
+ );
7924
8115
  addPackWords(db, packWordsOf(options.packs));
7925
8116
  return db;
7926
8117
  }
@@ -7978,7 +8169,10 @@ async function extractPythonProject(options) {
7978
8169
  (client) => (client.receiverConstructors ?? []).length > 0
7979
8170
  )
7980
8171
  );
7981
- const needsValues = discovers || mountsRouters || buildsReceivers || storagePatterns.length > 0 || modelQueries.length > 0;
8172
+ const envSites = bound.flatMap(
8173
+ (boundFile) => envNameSites(boundFile.file, boundFile.root, boundFile.module)
8174
+ );
8175
+ const needsValues = discovers || mountsRouters || buildsReceivers || envSites.length > 0 || storagePatterns.length > 0 || modelQueries.length > 0;
7982
8176
  const definitions = /* @__PURE__ */ new Map();
7983
8177
  timer.time("discover", () => {
7984
8178
  for (const { file, root, module: moduleBinding } of bound) {
@@ -7990,6 +8184,7 @@ async function extractPythonProject(options) {
7990
8184
  }
7991
8185
  if (needsValues) {
7992
8186
  bindEvaluator(db, { files: bound, definitions });
8187
+ bindEnvNameSites(db, envSites);
7993
8188
  }
7994
8189
  addPackWords(db, packWordsOf(options.packs));
7995
8190
  });
@@ -8078,7 +8273,7 @@ async function extractPythonProject(options) {
8078
8273
  }
8079
8274
  const loadTimeReads = timer.time(
8080
8275
  "discover",
8081
- () => envReadEffects(root, moduleBinding)
8276
+ () => envReadEffects(root, moduleBinding, db)
8082
8277
  );
8083
8278
  if (loadTimeReads.length > 0) {
8084
8279
  const summary = timer.time(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/adapter-python",
3
- "version": "0.31.1",
3
+ "version": "0.32.0",
4
4
  "type": "module",
5
5
  "description": "Read a Python codebase with suss and check what each FastAPI or Flask route returns against the clients, specs and infrastructure that depend on it.",
6
6
  "keywords": [
@@ -49,13 +49,13 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@suss/behavioral-ir": "0.31.1",
53
- "@suss/datalog": "0.31.1",
54
- "@suss/extractor": "0.31.1",
55
- "@suss/ir-core": "0.31.1",
56
- "@suss/resolution": "0.31.1",
57
- "@suss/sql": "0.31.1",
58
- "@suss/values": "0.31.1",
52
+ "@suss/behavioral-ir": "0.32.0",
53
+ "@suss/datalog": "0.32.0",
54
+ "@suss/extractor": "0.32.0",
55
+ "@suss/ir-core": "0.32.0",
56
+ "@suss/resolution": "0.32.0",
57
+ "@suss/sql": "0.32.0",
58
+ "@suss/values": "0.32.0",
59
59
  "web-tree-sitter": "^0.26.11"
60
60
  },
61
61
  "devDependencies": {