@suss/adapter-python 0.5.1 → 0.5.2

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.cjs CHANGED
@@ -666,6 +666,82 @@ function classifyDecorator(decoratorNode, scope) {
666
666
  }
667
667
  return { ...resolveCallee(expr, scope), args: [], keywordArgs: {}, range };
668
668
  }
669
+ function unwrapDecorator(decoratorNode, scope, ownModule, moduleDef) {
670
+ const expr = decoratorNode.namedChild(0);
671
+ if (expr === null || expr.type !== "call") {
672
+ return null;
673
+ }
674
+ const callee = field(expr, "function");
675
+ if (callee === null || callee.type !== "identifier") {
676
+ return null;
677
+ }
678
+ const wrapper = wrapperBehind(callee.text, scope, ownModule, moduleDef);
679
+ if (wrapper === null) {
680
+ return null;
681
+ }
682
+ const inner = singleReturnedCall(wrapper.node);
683
+ if (inner === null) {
684
+ return null;
685
+ }
686
+ const innerCallee = field(inner, "function");
687
+ if (innerCallee === null || innerCallee.type !== "attribute") {
688
+ return null;
689
+ }
690
+ if (!passesParametersThrough(wrapper.node, inner)) {
691
+ return null;
692
+ }
693
+ const resolved = resolveCallee(innerCallee, wrapper.module.moduleScope);
694
+ if (resolved.importedName === null) {
695
+ return null;
696
+ }
697
+ const argumentList = field(expr, "arguments");
698
+ const { args, keywordArgs } = argumentList?.type === "argument_list" ? readCallArguments(argumentList) : { args: [], keywordArgs: {} };
699
+ return {
700
+ ...resolved,
701
+ args,
702
+ keywordArgs,
703
+ range: rangeOf(decoratorNode),
704
+ objectModule: wrapper.module
705
+ };
706
+ }
707
+ function wrapperBehind(name, scope, ownModule, moduleDef) {
708
+ const binding = resolveName(scope, name);
709
+ if (binding?.kind === "functionDef") {
710
+ return { node: binding.node, module: ownModule };
711
+ }
712
+ if (binding?.kind === "importFrom") {
713
+ return moduleDef(
714
+ { module: binding.module, relativeLevel: binding.relativeLevel },
715
+ binding.importedName
716
+ );
717
+ }
718
+ return null;
719
+ }
720
+ function singleReturnedCall(def) {
721
+ const body = field(def, "body");
722
+ if (body === null) {
723
+ return null;
724
+ }
725
+ const statements = body.namedChildren.filter(
726
+ (child) => child !== null && child.type !== "comment"
727
+ );
728
+ if (statements.length !== 1 || statements[0]?.type !== "return_statement") {
729
+ return null;
730
+ }
731
+ const returned = statements[0].namedChildren[0];
732
+ return returned !== null && returned?.type === "call" ? returned : null;
733
+ }
734
+ function passesParametersThrough(def, inner) {
735
+ const params = field(def, "parameters");
736
+ const names = (params?.namedChildren ?? []).filter((child) => child?.type === "identifier").map((child) => child.text);
737
+ const argumentList = field(inner, "arguments");
738
+ const positional = (argumentList?.namedChildren ?? []).filter(
739
+ (child) => child !== null && child.type !== "keyword_argument"
740
+ );
741
+ return positional.length === names.length && positional.every(
742
+ (argument, at) => argument.type === "identifier" && argument.text === names[at]
743
+ );
744
+ }
669
745
 
670
746
  // src/discovery.ts
671
747
  var import_behavioral_ir = require("@suss/behavioral-ir");
@@ -17177,33 +17253,51 @@ function discoverUnits(root, module2, options) {
17177
17253
  }
17178
17254
  const { definition, decorators } = stripDecorators(stmt);
17179
17255
  for (const decoratorNode of decorators) {
17180
- const classification = classifyDecorator(
17181
- decoratorNode,
17182
- module2.moduleScope
17256
+ const direct = classifyDecorator(decoratorNode, module2.moduleScope);
17257
+ const classifications = direct.module !== null && acceptedByAnyPattern(direct.module, options) ? [direct] : [
17258
+ unwrapDecorator(
17259
+ decoratorNode,
17260
+ module2.moduleScope,
17261
+ module2,
17262
+ (spec, name) => options.routerIndex?.moduleDef(
17263
+ options.absoluteFile ?? options.filePath,
17264
+ spec,
17265
+ name
17266
+ ) ?? null
17267
+ )
17268
+ ].filter(
17269
+ (candidate) => candidate !== null
17183
17270
  );
17184
- if (classification.module === null) {
17185
- continue;
17186
- }
17187
- const decoratorModule = classification.module;
17188
- for (const pack of options.packs) {
17189
- for (const pattern of pack.discovery) {
17190
- units.push(
17191
- ...unitsFor(
17192
- pattern,
17193
- pack,
17194
- decoratorModule,
17195
- classification,
17196
- definition,
17197
- module2,
17198
- options
17199
- )
17200
- );
17271
+ for (const classification of classifications) {
17272
+ if (classification.module === null) {
17273
+ continue;
17274
+ }
17275
+ const decoratorModule = classification.module;
17276
+ for (const pack of options.packs) {
17277
+ for (const pattern of pack.discovery) {
17278
+ units.push(
17279
+ ...unitsFor(
17280
+ pattern,
17281
+ pack,
17282
+ decoratorModule,
17283
+ classification,
17284
+ definition,
17285
+ module2,
17286
+ options
17287
+ )
17288
+ );
17289
+ }
17201
17290
  }
17202
17291
  }
17203
17292
  }
17204
17293
  }
17205
17294
  return units;
17206
17295
  }
17296
+ function acceptedByAnyPattern(module2, options) {
17297
+ return options.packs.some(
17298
+ (pack) => pack.discovery.some((pattern) => pattern.importModule.includes(module2))
17299
+ );
17300
+ }
17207
17301
  function unitsFor(pattern, pack, decoratorModule, classification, definition, module2, options) {
17208
17302
  if (!pattern.importModule.includes(decoratorModule)) {
17209
17303
  return [];
@@ -17381,7 +17475,7 @@ function readRouterPrefix(pattern, classification, module2, options, range) {
17381
17475
  }
17382
17476
  const resolution = options.routerIndex.resolve(
17383
17477
  pattern,
17384
- module2,
17478
+ classification.objectModule ?? module2,
17385
17479
  classification.objectName
17386
17480
  );
17387
17481
  if (resolution.kind === "abstain") {
@@ -18126,7 +18220,26 @@ function buildRouterIndex(files, packs, resolverOptions) {
18126
18220
  }
18127
18221
  }
18128
18222
  }
18223
+ const byFile = /* @__PURE__ */ new Map();
18224
+ for (const file2 of files) {
18225
+ byFile.set(file2.file, file2);
18226
+ }
18129
18227
  return {
18228
+ moduleDef(fromFile, spec, name) {
18229
+ const resolution = resolveModule(fromFile, spec, resolverOptions);
18230
+ if (resolution.status !== "resolved") {
18231
+ return null;
18232
+ }
18233
+ const bound = byFile.get(resolution.file);
18234
+ if (bound === void 0) {
18235
+ return null;
18236
+ }
18237
+ const binding = bound.module.moduleScope.bindings.get(name);
18238
+ if (binding?.kind !== "functionDef") {
18239
+ return null;
18240
+ }
18241
+ return { node: binding.node, module: bound.module };
18242
+ },
18130
18243
  resolve(pattern, module2, objectName) {
18131
18244
  const index = byPattern.get(pattern);
18132
18245
  if (index === void 0) {
@@ -19015,6 +19128,7 @@ async function extractPythonProject(options) {
19015
19128
  const rawUnits = discoverUnits(root, moduleBinding, {
19016
19129
  packs: options.packs,
19017
19130
  filePath: displayPath,
19131
+ absoluteFile: file2,
19018
19132
  routerIndex,
19019
19133
  gapHandling,
19020
19134
  ...storagePatterns.length > 0 ? {
package/dist/index.d.cts CHANGED
@@ -146,6 +146,12 @@ interface DecoratorClassification {
146
146
  keywordArgs: Record<string, DecoratorArg>;
147
147
  /** Where the decorator is written. Anything we read out of its arguments uses this as its provenance. */
148
148
  range: Range;
149
+ /**
150
+ * The module the decorator's object lives in, when the decorator was read
151
+ * through a project wrapper written in another file. The router prefix is
152
+ * resolved there, since that is where the namespace is constructed.
153
+ */
154
+ objectModule?: ModuleBinding;
149
155
  }
150
156
  declare function classifyDecorator(decoratorNode: Node, scope: Scope): DecoratorClassification;
151
157
 
@@ -417,6 +423,18 @@ type RoutePrefixResolution = {
417
423
  };
418
424
  interface RouterIndex {
419
425
  resolve(pattern: PythonDiscoveryPattern, module: ModuleBinding, objectName: string): RoutePrefixResolution;
426
+ /**
427
+ * The module-level def a name in another module refers to, with that
428
+ * module's own bindings, so a decorator written through a project wrapper
429
+ * can be read where the wrapper is written.
430
+ */
431
+ moduleDef(fromFile: string, spec: {
432
+ module: string;
433
+ relativeLevel: number;
434
+ }, name: string): {
435
+ node: Node;
436
+ module: ModuleBinding;
437
+ } | null;
420
438
  }
421
439
  interface RouterIndexOptions extends ModuleResolverOptions {
422
440
  /** The project's facts, so a loop over a call can be settled by the rules. */
@@ -448,6 +466,8 @@ interface DiscoveryOptions {
448
466
  gapHandling?: ExtractorOptions["gapHandling"];
449
467
  /** What a pack needs to say a call talks to the database. Absent when no pack does. */
450
468
  storage?: StorageLookup | undefined;
469
+ /** The file's absolute path, which module resolution wants; `filePath` may be shortened for display. */
470
+ absoluteFile?: string | undefined;
451
471
  }
452
472
  declare function discoverUnits(root: Node, module: ModuleBinding, options: DiscoveryOptions): RawCodeStructure[];
453
473
 
package/dist/index.d.ts CHANGED
@@ -146,6 +146,12 @@ interface DecoratorClassification {
146
146
  keywordArgs: Record<string, DecoratorArg>;
147
147
  /** Where the decorator is written. Anything we read out of its arguments uses this as its provenance. */
148
148
  range: Range;
149
+ /**
150
+ * The module the decorator's object lives in, when the decorator was read
151
+ * through a project wrapper written in another file. The router prefix is
152
+ * resolved there, since that is where the namespace is constructed.
153
+ */
154
+ objectModule?: ModuleBinding;
149
155
  }
150
156
  declare function classifyDecorator(decoratorNode: Node, scope: Scope): DecoratorClassification;
151
157
 
@@ -417,6 +423,18 @@ type RoutePrefixResolution = {
417
423
  };
418
424
  interface RouterIndex {
419
425
  resolve(pattern: PythonDiscoveryPattern, module: ModuleBinding, objectName: string): RoutePrefixResolution;
426
+ /**
427
+ * The module-level def a name in another module refers to, with that
428
+ * module's own bindings, so a decorator written through a project wrapper
429
+ * can be read where the wrapper is written.
430
+ */
431
+ moduleDef(fromFile: string, spec: {
432
+ module: string;
433
+ relativeLevel: number;
434
+ }, name: string): {
435
+ node: Node;
436
+ module: ModuleBinding;
437
+ } | null;
420
438
  }
421
439
  interface RouterIndexOptions extends ModuleResolverOptions {
422
440
  /** The project's facts, so a loop over a call can be settled by the rules. */
@@ -448,6 +466,8 @@ interface DiscoveryOptions {
448
466
  gapHandling?: ExtractorOptions["gapHandling"];
449
467
  /** What a pack needs to say a call talks to the database. Absent when no pack does. */
450
468
  storage?: StorageLookup | undefined;
469
+ /** The file's absolute path, which module resolution wants; `filePath` may be shortened for display. */
470
+ absoluteFile?: string | undefined;
451
471
  }
452
472
  declare function discoverUnits(root: Node, module: ModuleBinding, options: DiscoveryOptions): RawCodeStructure[];
453
473
 
package/dist/index.js CHANGED
@@ -612,6 +612,82 @@ function classifyDecorator(decoratorNode, scope) {
612
612
  }
613
613
  return { ...resolveCallee(expr, scope), args: [], keywordArgs: {}, range };
614
614
  }
615
+ function unwrapDecorator(decoratorNode, scope, ownModule, moduleDef) {
616
+ const expr = decoratorNode.namedChild(0);
617
+ if (expr === null || expr.type !== "call") {
618
+ return null;
619
+ }
620
+ const callee = field(expr, "function");
621
+ if (callee === null || callee.type !== "identifier") {
622
+ return null;
623
+ }
624
+ const wrapper = wrapperBehind(callee.text, scope, ownModule, moduleDef);
625
+ if (wrapper === null) {
626
+ return null;
627
+ }
628
+ const inner = singleReturnedCall(wrapper.node);
629
+ if (inner === null) {
630
+ return null;
631
+ }
632
+ const innerCallee = field(inner, "function");
633
+ if (innerCallee === null || innerCallee.type !== "attribute") {
634
+ return null;
635
+ }
636
+ if (!passesParametersThrough(wrapper.node, inner)) {
637
+ return null;
638
+ }
639
+ const resolved = resolveCallee(innerCallee, wrapper.module.moduleScope);
640
+ if (resolved.importedName === null) {
641
+ return null;
642
+ }
643
+ const argumentList = field(expr, "arguments");
644
+ const { args, keywordArgs } = argumentList?.type === "argument_list" ? readCallArguments(argumentList) : { args: [], keywordArgs: {} };
645
+ return {
646
+ ...resolved,
647
+ args,
648
+ keywordArgs,
649
+ range: rangeOf(decoratorNode),
650
+ objectModule: wrapper.module
651
+ };
652
+ }
653
+ function wrapperBehind(name, scope, ownModule, moduleDef) {
654
+ const binding = resolveName(scope, name);
655
+ if (binding?.kind === "functionDef") {
656
+ return { node: binding.node, module: ownModule };
657
+ }
658
+ if (binding?.kind === "importFrom") {
659
+ return moduleDef(
660
+ { module: binding.module, relativeLevel: binding.relativeLevel },
661
+ binding.importedName
662
+ );
663
+ }
664
+ return null;
665
+ }
666
+ function singleReturnedCall(def) {
667
+ const body = field(def, "body");
668
+ if (body === null) {
669
+ return null;
670
+ }
671
+ const statements = body.namedChildren.filter(
672
+ (child) => child !== null && child.type !== "comment"
673
+ );
674
+ if (statements.length !== 1 || statements[0]?.type !== "return_statement") {
675
+ return null;
676
+ }
677
+ const returned = statements[0].namedChildren[0];
678
+ return returned !== null && returned?.type === "call" ? returned : null;
679
+ }
680
+ function passesParametersThrough(def, inner) {
681
+ const params = field(def, "parameters");
682
+ const names = (params?.namedChildren ?? []).filter((child) => child?.type === "identifier").map((child) => child.text);
683
+ const argumentList = field(inner, "arguments");
684
+ const positional = (argumentList?.namedChildren ?? []).filter(
685
+ (child) => child !== null && child.type !== "keyword_argument"
686
+ );
687
+ return positional.length === names.length && positional.every(
688
+ (argument, at) => argument.type === "identifier" && argument.text === names[at]
689
+ );
690
+ }
615
691
 
616
692
  // src/discovery.ts
617
693
  import { dispatchByType, restBinding } from "@suss/behavioral-ir";
@@ -17133,33 +17209,51 @@ function discoverUnits(root, module, options) {
17133
17209
  }
17134
17210
  const { definition, decorators } = stripDecorators(stmt);
17135
17211
  for (const decoratorNode of decorators) {
17136
- const classification = classifyDecorator(
17137
- decoratorNode,
17138
- module.moduleScope
17212
+ const direct = classifyDecorator(decoratorNode, module.moduleScope);
17213
+ const classifications = direct.module !== null && acceptedByAnyPattern(direct.module, options) ? [direct] : [
17214
+ unwrapDecorator(
17215
+ decoratorNode,
17216
+ module.moduleScope,
17217
+ module,
17218
+ (spec, name) => options.routerIndex?.moduleDef(
17219
+ options.absoluteFile ?? options.filePath,
17220
+ spec,
17221
+ name
17222
+ ) ?? null
17223
+ )
17224
+ ].filter(
17225
+ (candidate) => candidate !== null
17139
17226
  );
17140
- if (classification.module === null) {
17141
- continue;
17142
- }
17143
- const decoratorModule = classification.module;
17144
- for (const pack of options.packs) {
17145
- for (const pattern of pack.discovery) {
17146
- units.push(
17147
- ...unitsFor(
17148
- pattern,
17149
- pack,
17150
- decoratorModule,
17151
- classification,
17152
- definition,
17153
- module,
17154
- options
17155
- )
17156
- );
17227
+ for (const classification of classifications) {
17228
+ if (classification.module === null) {
17229
+ continue;
17230
+ }
17231
+ const decoratorModule = classification.module;
17232
+ for (const pack of options.packs) {
17233
+ for (const pattern of pack.discovery) {
17234
+ units.push(
17235
+ ...unitsFor(
17236
+ pattern,
17237
+ pack,
17238
+ decoratorModule,
17239
+ classification,
17240
+ definition,
17241
+ module,
17242
+ options
17243
+ )
17244
+ );
17245
+ }
17157
17246
  }
17158
17247
  }
17159
17248
  }
17160
17249
  }
17161
17250
  return units;
17162
17251
  }
17252
+ function acceptedByAnyPattern(module, options) {
17253
+ return options.packs.some(
17254
+ (pack) => pack.discovery.some((pattern) => pattern.importModule.includes(module))
17255
+ );
17256
+ }
17163
17257
  function unitsFor(pattern, pack, decoratorModule, classification, definition, module, options) {
17164
17258
  if (!pattern.importModule.includes(decoratorModule)) {
17165
17259
  return [];
@@ -17337,7 +17431,7 @@ function readRouterPrefix(pattern, classification, module, options, range) {
17337
17431
  }
17338
17432
  const resolution = options.routerIndex.resolve(
17339
17433
  pattern,
17340
- module,
17434
+ classification.objectModule ?? module,
17341
17435
  classification.objectName
17342
17436
  );
17343
17437
  if (resolution.kind === "abstain") {
@@ -18082,7 +18176,26 @@ function buildRouterIndex(files, packs, resolverOptions) {
18082
18176
  }
18083
18177
  }
18084
18178
  }
18179
+ const byFile = /* @__PURE__ */ new Map();
18180
+ for (const file2 of files) {
18181
+ byFile.set(file2.file, file2);
18182
+ }
18085
18183
  return {
18184
+ moduleDef(fromFile, spec, name) {
18185
+ const resolution = resolveModule(fromFile, spec, resolverOptions);
18186
+ if (resolution.status !== "resolved") {
18187
+ return null;
18188
+ }
18189
+ const bound = byFile.get(resolution.file);
18190
+ if (bound === void 0) {
18191
+ return null;
18192
+ }
18193
+ const binding = bound.module.moduleScope.bindings.get(name);
18194
+ if (binding?.kind !== "functionDef") {
18195
+ return null;
18196
+ }
18197
+ return { node: binding.node, module: bound.module };
18198
+ },
18086
18199
  resolve(pattern, module, objectName) {
18087
18200
  const index = byPattern.get(pattern);
18088
18201
  if (index === void 0) {
@@ -18971,6 +19084,7 @@ async function extractPythonProject(options) {
18971
19084
  const rawUnits = discoverUnits(root, moduleBinding, {
18972
19085
  packs: options.packs,
18973
19086
  filePath: displayPath,
19087
+ absoluteFile: file2,
18974
19088
  routerIndex,
18975
19089
  gapHandling,
18976
19090
  ...storagePatterns.length > 0 ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/adapter-python",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Python language adapter for suss — parses source with tree-sitter (WASM), resolves names with a lexical binder, and emits raw extractions for the suss pipeline.",
6
6
  "keywords": [
@@ -47,9 +47,9 @@
47
47
  "typecheck": "tsc --noEmit"
48
48
  },
49
49
  "dependencies": {
50
- "@suss/behavioral-ir": "0.5.1",
51
- "@suss/datalog": "0.5.1",
52
- "@suss/extractor": "0.5.1",
50
+ "@suss/behavioral-ir": "0.5.2",
51
+ "@suss/datalog": "0.5.2",
52
+ "@suss/extractor": "0.5.2",
53
53
  "web-tree-sitter": "^0.26.11"
54
54
  },
55
55
  "devDependencies": {