@suss/adapter-python 0.3.2-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 ADDED
@@ -0,0 +1,2317 @@
1
+ // src/annotations.ts
2
+ import { createHash } from "crypto";
3
+
4
+ // src/ast.ts
5
+ function rangeOf(node) {
6
+ return {
7
+ start: node.startPosition.row + 1,
8
+ end: node.endPosition.row + 1
9
+ };
10
+ }
11
+ function field(node, name) {
12
+ return node.childForFieldName(name);
13
+ }
14
+ function fields(node, name) {
15
+ return node.childrenForFieldName(name);
16
+ }
17
+ function isType(node, ...types) {
18
+ return types.includes(node.type);
19
+ }
20
+ function stringLiteralValue(node) {
21
+ if (node.type !== "string") {
22
+ return null;
23
+ }
24
+ let content = "";
25
+ for (const child of node.namedChildren) {
26
+ if (child === null) {
27
+ continue;
28
+ }
29
+ if (child.type === "string_content") {
30
+ content += child.text;
31
+ continue;
32
+ }
33
+ if (child.type === "interpolation") {
34
+ return null;
35
+ }
36
+ }
37
+ return content;
38
+ }
39
+ function bodyStatements(body) {
40
+ return body.namedChildren.filter((child) => child !== null);
41
+ }
42
+ function stripDecorators(node) {
43
+ if (node.type !== "decorated_definition") {
44
+ return { definition: node, decorators: [] };
45
+ }
46
+ const definition = field(node, "definition");
47
+ if (definition === null) {
48
+ throw new Error("decorated_definition with no definition field");
49
+ }
50
+ const decorators = node.namedChildren.filter(
51
+ (child) => child !== null && child.type === "decorator"
52
+ );
53
+ return { definition, decorators };
54
+ }
55
+
56
+ // src/scope.ts
57
+ function bindModule(root) {
58
+ const scopeFor = /* @__PURE__ */ new Map();
59
+ const openImports = [];
60
+ const moduleScope = {
61
+ kind: "module",
62
+ node: root,
63
+ parent: null,
64
+ bindings: /* @__PURE__ */ new Map()
65
+ };
66
+ scopeFor.set(root.id, moduleScope);
67
+ bindBody(root, moduleScope, scopeFor, openImports);
68
+ return { moduleScope, scopeFor, openImports };
69
+ }
70
+ function bindName(scope, name, binding) {
71
+ const existing = scope.bindings.get(name);
72
+ if (existing?.kind === "global" || existing?.kind === "nonlocal") {
73
+ return;
74
+ }
75
+ scope.bindings.set(name, binding);
76
+ }
77
+ function bindBody(bodyOwner, scope, scopeFor, openImports) {
78
+ const ctx = { scopeFor, openImports };
79
+ for (const stmt of bodyStatements(bodyOwner)) {
80
+ bindStatement(stmt, scope, ctx);
81
+ }
82
+ }
83
+ var STATEMENT_BINDERS = {
84
+ import_statement: bindImportStatement,
85
+ import_from_statement: bindImportFromStatement,
86
+ decorated_definition: bindDecoratedDefinition,
87
+ function_definition: bindFunctionDefinition,
88
+ class_definition: bindClassDefinition,
89
+ expression_statement: bindExpressionStatement,
90
+ global_statement: bindGlobalStatement,
91
+ nonlocal_statement: bindNonlocalStatement
92
+ };
93
+ function bindStatement(stmt, scope, ctx) {
94
+ const binder = STATEMENT_BINDERS[stmt.type];
95
+ binder?.(stmt, scope, ctx);
96
+ }
97
+ function bindImportStatement(stmt, scope) {
98
+ for (const nameNode of fields(stmt, "name")) {
99
+ if (nameNode.type === "aliased_import") {
100
+ const dotted = field(nameNode, "name");
101
+ const alias = field(nameNode, "alias");
102
+ if (dotted !== null && alias !== null) {
103
+ bindName(scope, alias.text, {
104
+ kind: "import",
105
+ module: dotted.text,
106
+ relativeLevel: 0,
107
+ localName: alias.text
108
+ });
109
+ }
110
+ continue;
111
+ }
112
+ if (nameNode.type === "dotted_name") {
113
+ const first = nameNode.namedChild(0);
114
+ if (first !== null) {
115
+ bindName(scope, first.text, {
116
+ kind: "import",
117
+ module: nameNode.text,
118
+ relativeLevel: 0,
119
+ localName: first.text
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+ function relativeImportParts(node) {
126
+ let relativeLevel = 0;
127
+ let module = "";
128
+ for (const child of node.namedChildren) {
129
+ if (child === null) {
130
+ continue;
131
+ }
132
+ if (child.type === "import_prefix") {
133
+ relativeLevel += child.text.length;
134
+ continue;
135
+ }
136
+ if (child.type === "dotted_name") {
137
+ module = child.text;
138
+ }
139
+ }
140
+ return { relativeLevel, module };
141
+ }
142
+ function bindImportFromStatement(stmt, scope, ctx) {
143
+ const moduleNode = field(stmt, "module_name");
144
+ if (moduleNode === null) {
145
+ return;
146
+ }
147
+ const { module, relativeLevel } = moduleNode.type === "relative_import" ? relativeImportParts(moduleNode) : { module: moduleNode.text, relativeLevel: 0 };
148
+ const hasWildcard = stmt.namedChildren.some(
149
+ (child) => child !== null && child.type === "wildcard_import"
150
+ );
151
+ if (hasWildcard) {
152
+ ctx.openImports.push(
153
+ relativeLevel > 0 ? `${".".repeat(relativeLevel)}${module}` : module
154
+ );
155
+ return;
156
+ }
157
+ for (const nameNode of fields(stmt, "name")) {
158
+ if (nameNode.type === "aliased_import") {
159
+ const original = field(nameNode, "name");
160
+ const alias = field(nameNode, "alias");
161
+ if (original !== null && alias !== null) {
162
+ bindName(scope, alias.text, {
163
+ kind: "importFrom",
164
+ module,
165
+ relativeLevel,
166
+ importedName: original.text
167
+ });
168
+ }
169
+ continue;
170
+ }
171
+ if (nameNode.type === "dotted_name") {
172
+ bindName(scope, nameNode.text, {
173
+ kind: "importFrom",
174
+ module,
175
+ relativeLevel,
176
+ importedName: nameNode.text
177
+ });
178
+ }
179
+ }
180
+ }
181
+ function bindDecoratedDefinition(stmt, scope, ctx) {
182
+ const { definition } = stripDecorators(stmt);
183
+ bindStatement(definition, scope, ctx);
184
+ }
185
+ function bindFunctionDefinition(stmt, scope, ctx) {
186
+ const nameNode = field(stmt, "name");
187
+ if (nameNode !== null) {
188
+ bindName(scope, nameNode.text, { kind: "functionDef", node: stmt });
189
+ }
190
+ const functionScope = {
191
+ kind: "function",
192
+ node: stmt,
193
+ parent: scope,
194
+ bindings: /* @__PURE__ */ new Map()
195
+ };
196
+ ctx.scopeFor.set(stmt.id, functionScope);
197
+ const parametersNode = field(stmt, "parameters");
198
+ if (parametersNode !== null) {
199
+ bindParameters(parametersNode, functionScope);
200
+ }
201
+ const bodyNode = field(stmt, "body");
202
+ if (bodyNode !== null) {
203
+ bindBody(bodyNode, functionScope, ctx.scopeFor, ctx.openImports);
204
+ }
205
+ }
206
+ function bindParameters(parametersNode, functionScope) {
207
+ for (const param of parametersNode.namedChildren) {
208
+ if (param === null) {
209
+ continue;
210
+ }
211
+ const name = parameterName(param);
212
+ if (name !== null) {
213
+ bindName(functionScope, name, { kind: "parameter" });
214
+ }
215
+ }
216
+ }
217
+ function parameterName(param) {
218
+ if (isType(param, "identifier")) {
219
+ return param.text;
220
+ }
221
+ if (isType(
222
+ param,
223
+ "typed_parameter",
224
+ "list_splat_pattern",
225
+ "dictionary_splat_pattern"
226
+ )) {
227
+ const inner = param.namedChildren.find(
228
+ (child) => child !== null && child.type === "identifier"
229
+ );
230
+ return inner?.text ?? null;
231
+ }
232
+ if (isType(param, "default_parameter", "typed_default_parameter")) {
233
+ const nameNode = field(param, "name");
234
+ return nameNode?.type === "identifier" ? nameNode.text : null;
235
+ }
236
+ return null;
237
+ }
238
+ function bindClassDefinition(stmt, scope, ctx) {
239
+ const nameNode = field(stmt, "name");
240
+ if (nameNode !== null) {
241
+ bindName(scope, nameNode.text, { kind: "classDef", node: stmt });
242
+ }
243
+ const classScope = {
244
+ kind: "class",
245
+ node: stmt,
246
+ parent: scope,
247
+ bindings: /* @__PURE__ */ new Map()
248
+ };
249
+ ctx.scopeFor.set(stmt.id, classScope);
250
+ const bodyNode = field(stmt, "body");
251
+ if (bodyNode !== null) {
252
+ bindBody(bodyNode, classScope, ctx.scopeFor, ctx.openImports);
253
+ }
254
+ }
255
+ function bindExpressionStatement(stmt, scope) {
256
+ const assignment = stmt.namedChildren.find(
257
+ (child) => child !== null && child.type === "assignment"
258
+ );
259
+ if (assignment === void 0 || assignment === null) {
260
+ return;
261
+ }
262
+ const left = field(assignment, "left");
263
+ if (left === null) {
264
+ return;
265
+ }
266
+ if (left.type === "identifier") {
267
+ bindName(scope, left.text, {
268
+ kind: "assignment",
269
+ value: field(assignment, "right")
270
+ });
271
+ }
272
+ }
273
+ function bindGlobalStatement(stmt, scope) {
274
+ for (const child of stmt.namedChildren) {
275
+ if (child !== null && child.type === "identifier") {
276
+ scope.bindings.set(child.text, { kind: "global" });
277
+ }
278
+ }
279
+ }
280
+ function bindNonlocalStatement(stmt, scope) {
281
+ for (const child of stmt.namedChildren) {
282
+ if (child !== null && child.type === "identifier") {
283
+ scope.bindings.set(child.text, { kind: "nonlocal" });
284
+ }
285
+ }
286
+ }
287
+ function moduleScopeOf(scope) {
288
+ let current = scope;
289
+ while (current.parent !== null) {
290
+ current = current.parent;
291
+ }
292
+ return current;
293
+ }
294
+ function resolveName(scope, name) {
295
+ let current = scope;
296
+ let first = true;
297
+ while (current !== null) {
298
+ if (current.kind !== "class" || first) {
299
+ const binding = current.bindings.get(name);
300
+ if (binding !== void 0) {
301
+ if (binding.kind === "global") {
302
+ return moduleScopeOf(scope).bindings.get(name) ?? null;
303
+ }
304
+ if (binding.kind === "nonlocal") {
305
+ return resolveNonlocal(current, name);
306
+ }
307
+ return binding;
308
+ }
309
+ }
310
+ first = false;
311
+ current = current.parent;
312
+ }
313
+ return null;
314
+ }
315
+ function resolveNonlocal(scope, name) {
316
+ let current = scope.parent;
317
+ while (current !== null) {
318
+ if (current.kind === "function") {
319
+ const binding = current.bindings.get(name);
320
+ if (binding !== void 0 && binding.kind !== "nonlocal" && binding.kind !== "global") {
321
+ return binding;
322
+ }
323
+ }
324
+ current = current.parent;
325
+ }
326
+ return null;
327
+ }
328
+
329
+ // src/annotations.ts
330
+ function createAnnotationContext(scopeFor) {
331
+ return { scopeFor, definitions: /* @__PURE__ */ new Map() };
332
+ }
333
+ function collectedDefinitions(ctx) {
334
+ const out = {};
335
+ let any = false;
336
+ for (const [key, shape] of ctx.definitions) {
337
+ if (shape !== null) {
338
+ out[key] = shape;
339
+ any = true;
340
+ }
341
+ }
342
+ return any ? out : null;
343
+ }
344
+ var BUILTIN_SCALARS = {
345
+ int: { type: "integer" },
346
+ float: { type: "number" },
347
+ str: { type: "text" },
348
+ bytes: { type: "text" },
349
+ bool: { type: "boolean" },
350
+ Any: { type: "unknown" },
351
+ object: { type: "unknown" }
352
+ };
353
+ var LIST_NAMES = /* @__PURE__ */ new Set(["list", "List", "Sequence", "Iterable"]);
354
+ var DICT_NAMES = /* @__PURE__ */ new Set(["dict", "Dict", "Mapping"]);
355
+ var TUPLE_SET_NAMES = /* @__PURE__ */ new Set([
356
+ "tuple",
357
+ "Tuple",
358
+ "set",
359
+ "Set",
360
+ "frozenset",
361
+ "FrozenSet"
362
+ ]);
363
+ function annotationToShape(typeNode, scope, ctx) {
364
+ const inner = typeNode.namedChild(0);
365
+ if (inner === null) {
366
+ return { type: "unknown" };
367
+ }
368
+ return shapeFromExpression(inner, scope, ctx);
369
+ }
370
+ var EXPRESSION_SHAPERS = {
371
+ identifier: (node, scope, ctx) => shapeFromName(node.text, scope, ctx),
372
+ none: () => ({ type: "null" }),
373
+ generic_type: shapeFromGenericType,
374
+ binary_operator: shapeFromBinaryOperator
375
+ };
376
+ function shapeFromExpression(node, scope, ctx) {
377
+ const handler = EXPRESSION_SHAPERS[node.type];
378
+ return handler !== void 0 ? handler(node, scope, ctx) : { type: "unknown" };
379
+ }
380
+ function shapeFromName(name, scope, ctx) {
381
+ const scalar = BUILTIN_SCALARS[name];
382
+ if (scalar !== void 0) {
383
+ return scalar;
384
+ }
385
+ if (LIST_NAMES.has(name)) {
386
+ return { type: "array", items: { type: "unknown" } };
387
+ }
388
+ if (DICT_NAMES.has(name)) {
389
+ return { type: "dictionary", values: { type: "unknown" } };
390
+ }
391
+ const binding = resolveName(scope, name);
392
+ if (binding?.kind === "classDef") {
393
+ return recordShapeRef(name, binding.node, ctx);
394
+ }
395
+ return { type: "ref", name };
396
+ }
397
+ function genericTypeArgs(node) {
398
+ const typeParameter = node.namedChildren.find(
399
+ (child) => child !== null && child.type === "type_parameter"
400
+ );
401
+ return typeParameter?.namedChildren.filter(
402
+ (child) => child !== null && child.type === "type"
403
+ ) ?? [];
404
+ }
405
+ function shapeFromGenericType(node, scope, ctx) {
406
+ const base = node.namedChild(0);
407
+ const args = genericTypeArgs(node);
408
+ const shapeOf = (arg) => arg !== void 0 ? annotationToShape(arg, scope, ctx) : { type: "unknown" };
409
+ const baseName = base?.type === "identifier" ? base.text : null;
410
+ if (baseName === "Optional") {
411
+ return { type: "union", variants: [shapeOf(args[0]), { type: "null" }] };
412
+ }
413
+ if (baseName === "Union") {
414
+ return { type: "union", variants: args.map((arg) => shapeOf(arg)) };
415
+ }
416
+ if (baseName !== null && LIST_NAMES.has(baseName)) {
417
+ return { type: "array", items: shapeOf(args[0]) };
418
+ }
419
+ if (baseName !== null && DICT_NAMES.has(baseName)) {
420
+ return { type: "dictionary", values: shapeOf(args[1] ?? args[0]) };
421
+ }
422
+ if (baseName !== null && TUPLE_SET_NAMES.has(baseName)) {
423
+ return { type: "array", items: shapeOf(args[0]) };
424
+ }
425
+ return { type: "ref", name: baseName ?? node.text };
426
+ }
427
+ function shapeFromBinaryOperator(node, scope, ctx) {
428
+ const operator = field(node, "operator");
429
+ const left = field(node, "left");
430
+ const right = field(node, "right");
431
+ if (operator?.text !== "|" || left === null || right === null) {
432
+ return { type: "unknown" };
433
+ }
434
+ return {
435
+ type: "union",
436
+ variants: [
437
+ shapeFromExpression(left, scope, ctx),
438
+ shapeFromExpression(right, scope, ctx)
439
+ ]
440
+ };
441
+ }
442
+ function recordShapeRef(name, classNode, ctx) {
443
+ const key = `${name}@${shortHash(classNode.text)}`;
444
+ const ref = { type: "ref", name, def: key };
445
+ if (ctx.definitions.has(key)) {
446
+ return ref;
447
+ }
448
+ ctx.definitions.set(key, null);
449
+ const bodyNode = field(classNode, "body");
450
+ const classScope = ctx.scopeFor.get(classNode.id);
451
+ ctx.definitions.set(
452
+ key,
453
+ bodyNode !== null && classScope !== void 0 ? recordShapeOf(bodyNode, classScope, ctx) : { type: "record", properties: {} }
454
+ );
455
+ return ref;
456
+ }
457
+ function recordShapeOf(bodyNode, classScope, ctx) {
458
+ const properties = {};
459
+ for (const stmt of bodyNode.namedChildren) {
460
+ if (stmt === null || stmt.type !== "expression_statement") {
461
+ continue;
462
+ }
463
+ const assignment = stmt.namedChildren.find(
464
+ (child) => child !== null && child.type === "assignment"
465
+ );
466
+ if (assignment === void 0 || assignment === null) {
467
+ continue;
468
+ }
469
+ const left = field(assignment, "left");
470
+ const typeNode = field(assignment, "type");
471
+ if (left === null || left.type !== "identifier" || typeNode === null) {
472
+ continue;
473
+ }
474
+ properties[left.text] = annotationToShape(typeNode, classScope, ctx);
475
+ }
476
+ return { type: "record", properties };
477
+ }
478
+ function shortHash(text) {
479
+ return createHash("sha1").update(text).digest("hex").slice(0, 12);
480
+ }
481
+
482
+ // src/decorators.ts
483
+ function readArg(node) {
484
+ const literal = stringLiteralValue(node);
485
+ if (literal !== null) {
486
+ return { kind: "string", value: literal };
487
+ }
488
+ if (node.type === "integer") {
489
+ const value = Number.parseInt(node.text, 10);
490
+ if (Number.isFinite(value)) {
491
+ return { kind: "number", value };
492
+ }
493
+ return { kind: "other" };
494
+ }
495
+ if (node.type === "none") {
496
+ return { kind: "none" };
497
+ }
498
+ if (node.type === "true" || node.type === "false") {
499
+ return { kind: "boolean", value: node.type === "true" };
500
+ }
501
+ if (node.type === "identifier") {
502
+ return { kind: "identifier", name: node.text };
503
+ }
504
+ if (node.type === "list") {
505
+ return {
506
+ kind: "list",
507
+ items: node.namedChildren.filter((child) => child !== null).map(readArg)
508
+ };
509
+ }
510
+ return { kind: "other" };
511
+ }
512
+ function readCallArguments(argumentList) {
513
+ const args = [];
514
+ const keywordArgs = {};
515
+ if (argumentList === null) {
516
+ return { args, keywordArgs };
517
+ }
518
+ for (const child of argumentList.namedChildren) {
519
+ if (child === null) {
520
+ continue;
521
+ }
522
+ if (child.type === "keyword_argument") {
523
+ const nameNode = field(child, "name");
524
+ const valueNode = field(child, "value");
525
+ if (nameNode !== null && valueNode !== null) {
526
+ keywordArgs[nameNode.text] = readArg(valueNode);
527
+ }
528
+ continue;
529
+ }
530
+ args.push(readArg(child));
531
+ }
532
+ return { args, keywordArgs };
533
+ }
534
+ var UNRESOLVED = {
535
+ importedName: null,
536
+ module: null,
537
+ objectName: null,
538
+ relativeLevel: 0
539
+ };
540
+ function resolveObjectModule(name, scope) {
541
+ const binding = resolveName(scope, name);
542
+ if (binding?.kind === "import") {
543
+ return { module: binding.module, relativeLevel: binding.relativeLevel };
544
+ }
545
+ if (binding?.kind === "assignment" && binding.value?.type === "call") {
546
+ const callee = field(binding.value, "function");
547
+ if (callee?.type === "identifier") {
548
+ const calleeBinding = resolveName(scope, callee.text);
549
+ if (calleeBinding?.kind === "importFrom") {
550
+ return {
551
+ module: calleeBinding.module,
552
+ relativeLevel: calleeBinding.relativeLevel
553
+ };
554
+ }
555
+ }
556
+ }
557
+ return null;
558
+ }
559
+ function resolveCallee(expr, scope) {
560
+ if (expr.type === "identifier") {
561
+ const binding = resolveName(scope, expr.text);
562
+ if (binding?.kind === "importFrom") {
563
+ return {
564
+ importedName: binding.importedName,
565
+ module: binding.module,
566
+ objectName: null,
567
+ relativeLevel: binding.relativeLevel
568
+ };
569
+ }
570
+ return UNRESOLVED;
571
+ }
572
+ if (expr.type === "attribute") {
573
+ const object = field(expr, "object");
574
+ const attribute = field(expr, "attribute");
575
+ if (object === null || attribute === null || object.type !== "identifier") {
576
+ return UNRESOLVED;
577
+ }
578
+ const origin = resolveObjectModule(object.text, scope);
579
+ if (origin === null) {
580
+ return UNRESOLVED;
581
+ }
582
+ return {
583
+ importedName: attribute.text,
584
+ module: origin.module,
585
+ objectName: object.text,
586
+ relativeLevel: origin.relativeLevel
587
+ };
588
+ }
589
+ return UNRESOLVED;
590
+ }
591
+ function classifyDecorator(decoratorNode, scope) {
592
+ const range = rangeOf(decoratorNode);
593
+ const expr = decoratorNode.namedChild(0);
594
+ if (expr === null) {
595
+ return { ...UNRESOLVED, args: [], keywordArgs: {}, range };
596
+ }
597
+ if (expr.type === "call") {
598
+ const callee = field(expr, "function");
599
+ const argumentList = field(expr, "arguments");
600
+ const resolved = callee !== null ? resolveCallee(callee, scope) : UNRESOLVED;
601
+ const { args, keywordArgs } = argumentList?.type === "argument_list" ? readCallArguments(argumentList) : { args: [], keywordArgs: {} };
602
+ return { ...resolved, args, keywordArgs, range };
603
+ }
604
+ return { ...resolveCallee(expr, scope), args: [], keywordArgs: {}, range };
605
+ }
606
+
607
+ // src/discovery.ts
608
+ import { dispatchByType, restBinding } from "@suss/behavioral-ir";
609
+ import {
610
+ absentReading,
611
+ ambiguousReading,
612
+ andThenReading,
613
+ firstWrittenReading,
614
+ mapReading,
615
+ unreadableReading,
616
+ valueToReadFurtherFrom,
617
+ writtenReading
618
+ } from "@suss/extractor";
619
+ function discoverUnits(root, module, options) {
620
+ const units = [];
621
+ for (const stmt of bodyStatements(root)) {
622
+ if (stmt.type !== "decorated_definition") {
623
+ continue;
624
+ }
625
+ const { definition, decorators } = stripDecorators(stmt);
626
+ for (const decoratorNode of decorators) {
627
+ const classification = classifyDecorator(
628
+ decoratorNode,
629
+ module.moduleScope
630
+ );
631
+ if (classification.module === null) {
632
+ continue;
633
+ }
634
+ const decoratorModule = classification.module;
635
+ for (const pack of options.packs) {
636
+ for (const pattern of pack.discovery) {
637
+ units.push(
638
+ ...unitsFor(
639
+ pattern,
640
+ pack,
641
+ decoratorModule,
642
+ classification,
643
+ definition,
644
+ module,
645
+ options
646
+ )
647
+ );
648
+ }
649
+ }
650
+ }
651
+ }
652
+ return units;
653
+ }
654
+ function unitsFor(pattern, pack, decoratorModule, classification, definition, module, options) {
655
+ if (!pattern.importModule.includes(decoratorModule)) {
656
+ return [];
657
+ }
658
+ const table = {
659
+ decoratedClassRoute: (p) => {
660
+ if (definition.type !== "class_definition" || classification.importedName !== p.decoratorName) {
661
+ return [];
662
+ }
663
+ return classRouteUnits(
664
+ p,
665
+ pack,
666
+ classification,
667
+ definition,
668
+ module,
669
+ options
670
+ );
671
+ },
672
+ decoratedFunctionRoute: (p) => {
673
+ if (definition.type !== "function_definition") {
674
+ return [];
675
+ }
676
+ const verb = classification.importedName !== null ? p.verbAttributeNames[classification.importedName] : void 0;
677
+ if (verb === void 0) {
678
+ return [];
679
+ }
680
+ return functionRouteUnits(
681
+ p,
682
+ pack,
683
+ verb,
684
+ classification,
685
+ definition,
686
+ module,
687
+ options
688
+ );
689
+ }
690
+ };
691
+ return dispatchByType(table, pattern);
692
+ }
693
+ function readPathArgument(classification) {
694
+ const first = classification.args[0];
695
+ if (first?.kind === "string") {
696
+ return writtenReading(first.value, classification.range);
697
+ }
698
+ return unreadableReading(
699
+ "The path in this route's decorator is not a string literal, so the binding names no path and nothing pairs with it",
700
+ classification.range
701
+ );
702
+ }
703
+ var NO_PATH_PARAMS = /* @__PURE__ */ new Set();
704
+ function templateReader(template) {
705
+ return (path4) => {
706
+ const names = [];
707
+ const canonical = path4.replace(template, (_match, name) => {
708
+ names.push(name);
709
+ return `{${name}}`;
710
+ });
711
+ return { path: canonical, paramNames: new Set(names) };
712
+ };
713
+ }
714
+ var PATH_TEMPLATE_READERS = {
715
+ braces: templateReader(/\{([A-Za-z_]\w*)(?::[A-Za-z_]\w*)?\}/g),
716
+ flaskConverters: templateReader(
717
+ /<(?:[A-Za-z_]\w*(?:\(.*?\))?:)?([A-Za-z_]\w*)>/g
718
+ )
719
+ };
720
+ function readPathTemplate(pattern, path4, range) {
721
+ const syntax = pattern.pathParamSyntax;
722
+ if (syntax === void 0) {
723
+ return writtenReading({ path: path4, paramNames: NO_PATH_PARAMS }, range);
724
+ }
725
+ const reader = PATH_TEMPLATE_READERS[syntax];
726
+ if (reader === void 0) {
727
+ return unreadableReading(
728
+ `The route's pack declares path-parameter syntax "${syntax}", which this adapter has no reader for, so the binding names no path and nothing pairs with it`,
729
+ range
730
+ );
731
+ }
732
+ return writtenReading(reader(path4), range);
733
+ }
734
+ function classRouteUnits(pattern, pack, classification, classNode, module, options) {
735
+ const pathArgument = readPathArgument(classification);
736
+ if (pathArgument.kind !== "written") {
737
+ return [];
738
+ }
739
+ const className = field(classNode, "name")?.text;
740
+ const bodyNode = field(classNode, "body");
741
+ const classScope = module.scopeFor.get(classNode.id);
742
+ if (className === void 0 || bodyNode === null || classScope === void 0) {
743
+ return [];
744
+ }
745
+ const routePath = readRoutePath(
746
+ pattern,
747
+ pathArgument,
748
+ classification,
749
+ module,
750
+ options
751
+ );
752
+ const units = [];
753
+ for (const stmt of bodyStatements(bodyNode)) {
754
+ const { definition: maybeMethod } = stripDecorators(stmt);
755
+ if (maybeMethod.type !== "function_definition") {
756
+ continue;
757
+ }
758
+ const methodName = field(maybeMethod, "name")?.text;
759
+ const verb = methodName !== void 0 ? pattern.verbMethodNames[methodName] : void 0;
760
+ if (verb === void 0 || methodName === void 0) {
761
+ continue;
762
+ }
763
+ units.push(
764
+ ...routeUnitOrAbstention(
765
+ {
766
+ pack,
767
+ name: `${className}.${methodName}`,
768
+ exportPath: [className, methodName],
769
+ method: verb,
770
+ routePath,
771
+ requestBodyFromAnnotatedClass: pattern.annotatedClassIsRequestBody === true,
772
+ injectedCallees: new Set(pattern.injectedParameterCallees ?? []),
773
+ definitionNode: maybeMethod,
774
+ enclosingScope: classScope,
775
+ module,
776
+ filePath: options.filePath,
777
+ skipReceiverParam: true,
778
+ responseShape: absentReading,
779
+ statusCode: defaultedStatus(
780
+ readReturnedStatus(pattern, maybeMethod),
781
+ pattern
782
+ )
783
+ },
784
+ options
785
+ )
786
+ );
787
+ }
788
+ return units;
789
+ }
790
+ function readRoutePath(pattern, pathArgument, classification, module, options) {
791
+ const composed = andThenReading(
792
+ pathArgument,
793
+ (literal, range) => composeRoutePath(pattern, classification, module, options, literal, range)
794
+ );
795
+ return andThenReading(
796
+ composed,
797
+ (path4, range) => readPathTemplate(pattern, path4, range)
798
+ );
799
+ }
800
+ function composeRoutePath(pattern, classification, module, options, literal, range) {
801
+ const prefix = readRouterPrefix(
802
+ pattern,
803
+ classification,
804
+ module,
805
+ options,
806
+ range
807
+ );
808
+ if (prefix.kind === "absent") {
809
+ return writtenReading(servedSpelling(pattern, literal), range);
810
+ }
811
+ return mapReading(
812
+ prefix,
813
+ (value) => servedSpelling(pattern, value + literal)
814
+ );
815
+ }
816
+ var REPEATED_SLASH_READERS = {
817
+ kept: (path4) => path4,
818
+ merged: (path4) => path4.replace(/\/{2,}/g, "/")
819
+ };
820
+ function servedSpelling(pattern, path4) {
821
+ return REPEATED_SLASH_READERS[pattern.pathRepeatedSlashes ?? "kept"](path4);
822
+ }
823
+ function readRouterPrefix(pattern, classification, module, options, range) {
824
+ if (pattern.routerComposition === void 0 || options.routerIndex === void 0 || classification.objectName === null) {
825
+ return absentReading;
826
+ }
827
+ const resolution = options.routerIndex.resolve(
828
+ pattern,
829
+ module,
830
+ classification.objectName
831
+ );
832
+ if (resolution.kind === "abstain") {
833
+ return unreadableReading(
834
+ `The router this route is declared on ${resolution.reason}, so the binding names no path and nothing pairs with it`,
835
+ range
836
+ );
837
+ }
838
+ if (resolution.kind === "composed") {
839
+ return writtenReading(resolution.value, range);
840
+ }
841
+ return absentReading;
842
+ }
843
+ function readStatusCode(pattern, classification) {
844
+ if (pattern.statusCodeKeyword === void 0) {
845
+ return absentReading;
846
+ }
847
+ const arg = classification.keywordArgs[pattern.statusCodeKeyword];
848
+ if (arg === void 0) {
849
+ return absentReading;
850
+ }
851
+ if (arg.kind === "number") {
852
+ return writtenReading(arg.value, classification.range);
853
+ }
854
+ return unreadableReading(
855
+ "The status this route's decorator states is not a literal number, so the response claims no status",
856
+ classification.range
857
+ );
858
+ }
859
+ function returnStatements(node, found = []) {
860
+ for (const child of node?.namedChildren ?? []) {
861
+ if (child === null) {
862
+ continue;
863
+ }
864
+ if (child.type === "function_definition" || child.type === "lambda") {
865
+ continue;
866
+ }
867
+ if (child.type === "return_statement") {
868
+ found.push(child);
869
+ }
870
+ returnStatements(child, found);
871
+ }
872
+ return found;
873
+ }
874
+ function statusFromReturnedValue(node) {
875
+ if (node === void 0) {
876
+ return null;
877
+ }
878
+ if (node.type === "integer") {
879
+ const parsed = Number.parseInt(node.text, 10);
880
+ return Number.isNaN(parsed) ? null : parsed;
881
+ }
882
+ const text = stringLiteralValue(node);
883
+ if (text === null) {
884
+ return null;
885
+ }
886
+ const digits = /^(\d{3})\b/.exec(text.trim());
887
+ return digits === null ? null : Number.parseInt(digits[1], 10);
888
+ }
889
+ function statusOfReturn(statement) {
890
+ const returned = statement.namedChildren[0];
891
+ if (returned?.type !== "expression_list") {
892
+ return { kind: "none" };
893
+ }
894
+ const status = statusFromReturnedValue(returned.namedChildren[1]);
895
+ return status === null ? { kind: "unreadable" } : { kind: "status", value: status };
896
+ }
897
+ function readReturnedStatus(pattern, definitionNode) {
898
+ if (pattern.statusFromReturnedTuple !== true) {
899
+ return absentReading;
900
+ }
901
+ const range = rangeOf(definitionNode);
902
+ const statuses = /* @__PURE__ */ new Set();
903
+ let plainReturns = 0;
904
+ let unreadable = 0;
905
+ for (const statement of returnStatements(field(definitionNode, "body"))) {
906
+ const read = statusOfReturn(statement);
907
+ if (read.kind === "status") {
908
+ statuses.add(read.value);
909
+ } else if (read.kind === "none") {
910
+ plainReturns += 1;
911
+ } else {
912
+ unreadable += 1;
913
+ }
914
+ }
915
+ if (unreadable > 0) {
916
+ return unreadableReading(
917
+ "This route returns a status this reading cannot resolve to a number, so no status is claimed for it",
918
+ range
919
+ );
920
+ }
921
+ if (statuses.size === 0) {
922
+ return absentReading;
923
+ }
924
+ const candidates = [...statuses];
925
+ if (statuses.size > 1 || plainReturns > 0) {
926
+ return ambiguousReading(
927
+ candidates,
928
+ "This route returns more than one status depending on which branch runs, and nothing here reads which, so no status is claimed for it",
929
+ range
930
+ );
931
+ }
932
+ return writtenReading(candidates[0], range);
933
+ }
934
+ function declaredOrReturnedStatus(declared, returned) {
935
+ return declared.kind === "absent" ? returned : declared;
936
+ }
937
+ function defaultedStatus(reading, pattern) {
938
+ return {
939
+ reading,
940
+ ...pattern.defaultStatusCode !== void 0 ? { libraryDefault: pattern.defaultStatusCode } : {}
941
+ };
942
+ }
943
+ function readResponseModel(pattern, classification, module, ctx) {
944
+ if (pattern.responseModelKeyword === void 0) {
945
+ return absentReading;
946
+ }
947
+ const arg = classification.keywordArgs[pattern.responseModelKeyword];
948
+ if (arg === void 0) {
949
+ return absentReading;
950
+ }
951
+ if (arg.kind === "identifier") {
952
+ return writtenReading(
953
+ shapeFromName(arg.name, module.moduleScope, ctx),
954
+ classification.range
955
+ );
956
+ }
957
+ return unreadableReading(
958
+ "The response model this route's decorator states is not a name, so the response claims no body shape",
959
+ classification.range
960
+ );
961
+ }
962
+ function readReturnAnnotation(definitionNode, scope, ctx) {
963
+ const returnTypeNode = field(definitionNode, "return_type");
964
+ if (returnTypeNode === null) {
965
+ return absentReading;
966
+ }
967
+ return writtenReading(
968
+ annotationToShape(returnTypeNode, scope, ctx),
969
+ rangeOf(returnTypeNode)
970
+ );
971
+ }
972
+ function functionRouteUnits(pattern, pack, verb, classification, functionNode, module, options) {
973
+ const functionName = field(functionNode, "name")?.text;
974
+ if (functionName === void 0) {
975
+ return [];
976
+ }
977
+ const ctx = createAnnotationContext(module.scopeFor);
978
+ return routeUnitOrAbstention(
979
+ {
980
+ pack,
981
+ name: functionName,
982
+ exportPath: [functionName],
983
+ method: verb,
984
+ routePath: readRoutePath(
985
+ pattern,
986
+ readPathArgument(classification),
987
+ classification,
988
+ module,
989
+ options
990
+ ),
991
+ requestBodyFromAnnotatedClass: pattern.annotatedClassIsRequestBody === true,
992
+ injectedCallees: new Set(pattern.injectedParameterCallees ?? []),
993
+ definitionNode: functionNode,
994
+ enclosingScope: module.moduleScope,
995
+ module,
996
+ filePath: options.filePath,
997
+ skipReceiverParam: false,
998
+ responseShape: readResponseModel(pattern, classification, module, ctx),
999
+ statusCode: defaultedStatus(
1000
+ declaredOrReturnedStatus(
1001
+ readStatusCode(pattern, classification),
1002
+ readReturnedStatus(pattern, functionNode)
1003
+ ),
1004
+ pattern
1005
+ ),
1006
+ definitionsCtx: ctx
1007
+ },
1008
+ options
1009
+ );
1010
+ }
1011
+ function readResponseShape(declared, readAnnotation) {
1012
+ if (declared.kind === "written") {
1013
+ return { reading: declared, passedOver: [] };
1014
+ }
1015
+ return firstWrittenReading([declared, readAnnotation()]);
1016
+ }
1017
+ function routeUnitOrAbstention(unitOptions, options) {
1018
+ try {
1019
+ return [buildRouteUnit(unitOptions)];
1020
+ } catch (error) {
1021
+ if (options.gapHandling === "strict") {
1022
+ throw error;
1023
+ }
1024
+ return [unbuiltRouteUnit(unitOptions, error)];
1025
+ }
1026
+ }
1027
+ function nullIfEmpty(value) {
1028
+ return value === "" ? null : value;
1029
+ }
1030
+ function unbuiltRouteUnit(unitOptions, error) {
1031
+ const range = rangeOf(unitOptions.definitionNode);
1032
+ const message = error instanceof Error ? error.message : String(error);
1033
+ return {
1034
+ identity: {
1035
+ name: unitOptions.name,
1036
+ nameKind: "binding",
1037
+ kind: "handler",
1038
+ file: unitOptions.filePath,
1039
+ range,
1040
+ exportName: unitOptions.exportPath[0] ?? unitOptions.name,
1041
+ exportPath: unitOptions.exportPath
1042
+ },
1043
+ boundaryBinding: restBinding({
1044
+ transport: unitOptions.pack.protocol,
1045
+ method: nullIfEmpty(unitOptions.method),
1046
+ path: null,
1047
+ recognition: unitOptions.pack.name
1048
+ }),
1049
+ parameters: [],
1050
+ branches: [],
1051
+ bodyContent: "statements",
1052
+ dependencyCalls: [],
1053
+ declaredContract: null,
1054
+ readings: [
1055
+ unreadableReading(
1056
+ `This route could not be read into a unit (${message}), so the binding names no path and nothing pairs with it`,
1057
+ range
1058
+ )
1059
+ ]
1060
+ };
1061
+ }
1062
+ function buildRouteUnit(options) {
1063
+ const {
1064
+ pack,
1065
+ name,
1066
+ exportPath,
1067
+ method,
1068
+ routePath,
1069
+ requestBodyFromAnnotatedClass,
1070
+ injectedCallees,
1071
+ definitionNode,
1072
+ enclosingScope,
1073
+ module,
1074
+ filePath,
1075
+ skipReceiverParam,
1076
+ statusCode,
1077
+ definitionsCtx
1078
+ } = options;
1079
+ const template = valueToReadFurtherFrom(routePath);
1080
+ const ctx = definitionsCtx ?? createAnnotationContext(module.scopeFor);
1081
+ const parameters = readParameters(
1082
+ definitionNode,
1083
+ enclosingScope,
1084
+ ctx,
1085
+ template?.paramNames ?? null,
1086
+ requestBodyFromAnnotatedClass,
1087
+ injectedCallees,
1088
+ skipReceiverParam
1089
+ );
1090
+ const responseShape = readResponseShape(
1091
+ options.responseShape,
1092
+ () => readReturnAnnotation(definitionNode, enclosingScope, ctx)
1093
+ );
1094
+ const branches = [];
1095
+ if (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent") {
1096
+ branches.push({
1097
+ conditions: [],
1098
+ terminal: {
1099
+ kind: "response",
1100
+ // These stay null here. The extractor fills them in from
1101
+ // `statusCodeReading` and `bodyShapeReading` below.
1102
+ statusCode: null,
1103
+ body: { typeText: null, shape: null },
1104
+ exceptionType: null,
1105
+ message: null,
1106
+ component: null,
1107
+ renderTree: null,
1108
+ delegateTarget: null,
1109
+ emitEvent: null,
1110
+ location: rangeOf(definitionNode)
1111
+ },
1112
+ statusCodeReading: statusCode,
1113
+ bodyShapeReading: { reading: responseShape.reading },
1114
+ effects: [],
1115
+ location: rangeOf(definitionNode),
1116
+ isDefault: true
1117
+ });
1118
+ }
1119
+ const bodyNode = field(definitionNode, "body");
1120
+ return {
1121
+ identity: {
1122
+ name,
1123
+ nameKind: "binding",
1124
+ kind: "handler",
1125
+ file: filePath,
1126
+ range: rangeOf(definitionNode),
1127
+ exportName: exportPath[0] ?? name,
1128
+ exportPath
1129
+ },
1130
+ boundaryBinding: restBinding({
1131
+ transport: pack.protocol,
1132
+ method,
1133
+ path: template?.path ?? null,
1134
+ recognition: pack.name
1135
+ }),
1136
+ parameters,
1137
+ branches,
1138
+ bodyContent: bodyNode !== null ? bodyContentOf(bodyNode) : "statements",
1139
+ dependencyCalls: [],
1140
+ declaredContract: null,
1141
+ readings: [
1142
+ routePath,
1143
+ ...unreadRoleReadings(parameters, rangeOf(definitionNode)),
1144
+ ...responseShape.passedOver
1145
+ ],
1146
+ ...collectedDefinitions(ctx) !== null ? { definitions: collectedDefinitions(ctx) } : {}
1147
+ };
1148
+ }
1149
+ function unreadRoleReadings(parameters, range) {
1150
+ if (!parameters.some((parameter) => parameter.role === null)) {
1151
+ return [];
1152
+ }
1153
+ return [
1154
+ unreadableReading(
1155
+ "Nothing read this route's path, so its parameters name no role and a path parameter here does not read as one",
1156
+ range
1157
+ )
1158
+ ];
1159
+ }
1160
+ function bodyContentOf(bodyNode) {
1161
+ for (const stmt of bodyStatements(bodyNode)) {
1162
+ if (stmt.type === "pass_statement") {
1163
+ continue;
1164
+ }
1165
+ if (stmt.type === "expression_statement" && stmt.namedChild(0)?.type === "string") {
1166
+ continue;
1167
+ }
1168
+ return "statements";
1169
+ }
1170
+ return "empty";
1171
+ }
1172
+ function readParameters(definitionNode, scope, ctx, pathParamNames, requestBodyFromAnnotatedClass, injectedCallees, skipReceiverParam) {
1173
+ const parametersNode = field(definitionNode, "parameters");
1174
+ if (parametersNode === null) {
1175
+ return [];
1176
+ }
1177
+ const out = [];
1178
+ let position = 0;
1179
+ for (const param of parametersNode.namedChildren) {
1180
+ if (param === null) {
1181
+ continue;
1182
+ }
1183
+ if (skipReceiverParam && position === 0 && isReceiverParam(param)) {
1184
+ position += 1;
1185
+ continue;
1186
+ }
1187
+ const parsed = readParameter(
1188
+ param,
1189
+ scope,
1190
+ ctx,
1191
+ pathParamNames,
1192
+ requestBodyFromAnnotatedClass,
1193
+ injectedCallees,
1194
+ position
1195
+ );
1196
+ if (parsed !== null) {
1197
+ out.push(parsed);
1198
+ }
1199
+ position += 1;
1200
+ }
1201
+ return out;
1202
+ }
1203
+ function isReceiverParam(param) {
1204
+ return param.type === "identifier" && (param.text === "self" || param.text === "cls");
1205
+ }
1206
+ function readParameter(param, scope, ctx, pathParamNames, requestBodyFromAnnotatedClass, injectedCallees, position) {
1207
+ const info = parameterNameAndType(param);
1208
+ if (info === null) {
1209
+ return null;
1210
+ }
1211
+ const { name, typeNode } = info;
1212
+ const shape = typeNode !== null ? annotationToShape(typeNode, scope, ctx) : null;
1213
+ const role = isInjectedParameter(param, injectedCallees) ? null : roleOf(name, shape, pathParamNames, requestBodyFromAnnotatedClass);
1214
+ return {
1215
+ name,
1216
+ position,
1217
+ role,
1218
+ typeText: typeNode !== null ? typeNode.text : null
1219
+ };
1220
+ }
1221
+ function roleOf(name, shape, pathParamNames, requestBodyFromAnnotatedClass) {
1222
+ if (pathParamNames?.has(name) === true) {
1223
+ return "pathParams";
1224
+ }
1225
+ if (requestBodyFromAnnotatedClass && shape !== null && shape.type === "ref" && shape.def !== void 0) {
1226
+ return "requestBody";
1227
+ }
1228
+ if (pathParamNames === null) {
1229
+ return null;
1230
+ }
1231
+ return "queryParams";
1232
+ }
1233
+ function calleeName(call) {
1234
+ const callee = field(call, "function");
1235
+ if (callee === null) {
1236
+ return null;
1237
+ }
1238
+ if (callee.type === "identifier") {
1239
+ return callee.text;
1240
+ }
1241
+ if (callee.type === "attribute") {
1242
+ return field(callee, "attribute")?.text ?? null;
1243
+ }
1244
+ return null;
1245
+ }
1246
+ function isInjectedParameter(param, injectedCallees) {
1247
+ if (injectedCallees.size === 0) {
1248
+ return false;
1249
+ }
1250
+ const callsAnInjector = (node) => {
1251
+ if (node === null) {
1252
+ return false;
1253
+ }
1254
+ const name = calleeName(node);
1255
+ return node.type === "call" && name !== null && injectedCallees.has(name);
1256
+ };
1257
+ if (callsAnInjector(field(param, "value"))) {
1258
+ return true;
1259
+ }
1260
+ const containsInjectorCall = (node, depth) => {
1261
+ if (node === null || depth > 6) {
1262
+ return false;
1263
+ }
1264
+ if (callsAnInjector(node)) {
1265
+ return true;
1266
+ }
1267
+ return node.namedChildren.some(
1268
+ (child) => containsInjectorCall(child, depth + 1)
1269
+ );
1270
+ };
1271
+ return containsInjectorCall(field(param, "type"), 0);
1272
+ }
1273
+ function parameterNameAndType(param) {
1274
+ if (param.type === "identifier") {
1275
+ return { name: param.text, typeNode: null };
1276
+ }
1277
+ if (param.type === "typed_parameter") {
1278
+ const inner = param.namedChildren.find(
1279
+ (child) => child !== null && child.type === "identifier"
1280
+ );
1281
+ return inner !== void 0 ? { name: inner.text, typeNode: field(param, "type") } : null;
1282
+ }
1283
+ if (param.type === "default_parameter") {
1284
+ const nameNode = field(param, "name");
1285
+ return nameNode?.type === "identifier" ? { name: nameNode.text, typeNode: null } : null;
1286
+ }
1287
+ if (param.type === "typed_default_parameter") {
1288
+ const nameNode = field(param, "name");
1289
+ return nameNode !== null ? { name: nameNode.text, typeNode: field(param, "type") } : null;
1290
+ }
1291
+ return null;
1292
+ }
1293
+
1294
+ // src/moduleResolver.ts
1295
+ import fs from "fs";
1296
+ import path from "path";
1297
+ function isWithinRoot(dir, root) {
1298
+ const relative = path.relative(root, dir);
1299
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
1300
+ }
1301
+ function isInsideAnyRoot(dir, roots) {
1302
+ return roots.some((root) => isWithinRoot(dir, root));
1303
+ }
1304
+ function candidatesUnderRoot(root, dotted) {
1305
+ if (dotted === "") {
1306
+ const initFile = path.join(root, "__init__.py");
1307
+ return fs.existsSync(initFile) ? [initFile] : [];
1308
+ }
1309
+ const base = path.join(root, ...dotted.split("."));
1310
+ const found = [];
1311
+ const moduleFile = `${base}.py`;
1312
+ if (fs.existsSync(moduleFile)) {
1313
+ found.push(moduleFile);
1314
+ }
1315
+ const packageFile = path.join(base, "__init__.py");
1316
+ if (fs.existsSync(packageFile)) {
1317
+ found.push(packageFile);
1318
+ }
1319
+ return found;
1320
+ }
1321
+ function resolveAbsoluteModule(dotted, options) {
1322
+ const byRoot = [];
1323
+ for (const root of options.roots) {
1324
+ const candidates = candidatesUnderRoot(root, dotted);
1325
+ if (candidates.length > 0) {
1326
+ byRoot.push(candidates[0]);
1327
+ }
1328
+ }
1329
+ if (byRoot.length === 0) {
1330
+ return { status: "unresolved", reason: "external" };
1331
+ }
1332
+ if (byRoot.length > 1) {
1333
+ return { status: "unresolved", reason: "ambiguous" };
1334
+ }
1335
+ return { status: "resolved", file: byRoot[0] };
1336
+ }
1337
+ function resolveRelativeModule(importingFile, spec, options) {
1338
+ let dir = path.dirname(importingFile);
1339
+ if (!isInsideAnyRoot(dir, options.roots)) {
1340
+ return { status: "unresolved", reason: "outsideRoots" };
1341
+ }
1342
+ for (let i = 1; i < spec.relativeLevel; i++) {
1343
+ dir = path.dirname(dir);
1344
+ if (!isInsideAnyRoot(dir, options.roots)) {
1345
+ return { status: "unresolved", reason: "outsideRoots" };
1346
+ }
1347
+ }
1348
+ const candidates = candidatesUnderRoot(dir, spec.module);
1349
+ if (candidates.length === 0) {
1350
+ return { status: "unresolved", reason: "external" };
1351
+ }
1352
+ return { status: "resolved", file: candidates[0] };
1353
+ }
1354
+ function resolveModule(importingFile, spec, options) {
1355
+ if (spec.relativeLevel > 0) {
1356
+ return resolveRelativeModule(importingFile, spec, options);
1357
+ }
1358
+ return resolveAbsoluteModule(spec.module, options);
1359
+ }
1360
+
1361
+ // src/facts.ts
1362
+ function unitKey(filePath, range, name) {
1363
+ return `${filePath}:${range.start}-${range.end}#${name}`;
1364
+ }
1365
+ function emitEntryFact(db, filePath, range, name) {
1366
+ db.add("entry", [unitKey(filePath, range, name)]);
1367
+ }
1368
+ function importedModuleText(module, relativeLevel) {
1369
+ return relativeLevel > 0 ? `${".".repeat(relativeLevel)}${module}` : module;
1370
+ }
1371
+ function emitModuleImportFacts(db, filePath, module, resolverOptions) {
1372
+ for (const binding of module.moduleScope.bindings.values()) {
1373
+ if (binding.kind !== "import" && binding.kind !== "importFrom") {
1374
+ continue;
1375
+ }
1376
+ const moduleText = importedModuleText(
1377
+ binding.module,
1378
+ binding.relativeLevel
1379
+ );
1380
+ const resolution = resolveModule(
1381
+ filePath,
1382
+ { module: binding.module, relativeLevel: binding.relativeLevel },
1383
+ resolverOptions
1384
+ );
1385
+ db.add("pyImport", [
1386
+ filePath,
1387
+ moduleText,
1388
+ resolution.status === "resolved" ? "resolved" : resolution.reason
1389
+ ]);
1390
+ if (resolution.status === "resolved") {
1391
+ db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
1392
+ }
1393
+ }
1394
+ for (const openModule of module.openImports) {
1395
+ db.add("pyOpenImport", [filePath, openModule]);
1396
+ }
1397
+ }
1398
+
1399
+ // src/parser.ts
1400
+ import fs2 from "fs";
1401
+ import path2 from "path";
1402
+ import { fileURLToPath } from "url";
1403
+ import { Language, Parser } from "web-tree-sitter";
1404
+ var GRAMMAR_PATH = path2.join(
1405
+ path2.dirname(fileURLToPath(import.meta.url)),
1406
+ "..",
1407
+ "grammar",
1408
+ "tree-sitter-python.wasm"
1409
+ );
1410
+ var languagePromise = null;
1411
+ function loadLanguage() {
1412
+ languagePromise ??= (async () => {
1413
+ if (!fs2.existsSync(GRAMMAR_PATH)) {
1414
+ throw new Error(
1415
+ `Python grammar not found at ${GRAMMAR_PATH}. See grammar/README.md.`
1416
+ );
1417
+ }
1418
+ await Parser.init();
1419
+ return Language.load(GRAMMAR_PATH);
1420
+ })();
1421
+ return languagePromise;
1422
+ }
1423
+ async function parsePython(source) {
1424
+ const language = await loadLanguage();
1425
+ const parser = new Parser();
1426
+ parser.setLanguage(language);
1427
+ try {
1428
+ const tree = parser.parse(source);
1429
+ if (tree === null) {
1430
+ throw new Error("tree-sitter produced no tree for this source");
1431
+ }
1432
+ return tree;
1433
+ } finally {
1434
+ parser.delete();
1435
+ }
1436
+ }
1437
+
1438
+ // src/project.ts
1439
+ import fs3 from "fs";
1440
+ import path3 from "path";
1441
+ import { Database } from "@suss/datalog";
1442
+ import { assembleSummary } from "@suss/extractor";
1443
+
1444
+ // src/routers.ts
1445
+ var UNSTATED_PREFIX = { kind: "unstated" };
1446
+ var UNREADABLE_PREFIX = { kind: "unreadable" };
1447
+ var MODULE_SITE = { kind: "module" };
1448
+ var NOT_ROUTER = { kind: "notRouter" };
1449
+ function buildRouterIndex(files, packs, resolverOptions) {
1450
+ const byPattern = /* @__PURE__ */ new Map();
1451
+ for (const pack of packs) {
1452
+ for (const pattern of pack.discovery) {
1453
+ if (pattern.routerComposition !== void 0) {
1454
+ byPattern.set(
1455
+ pattern,
1456
+ buildPatternIndex(
1457
+ files,
1458
+ pattern.importModule,
1459
+ pattern.routerComposition,
1460
+ resolverOptions
1461
+ )
1462
+ );
1463
+ }
1464
+ }
1465
+ }
1466
+ return {
1467
+ resolve(pattern, module, objectName) {
1468
+ const index = byPattern.get(pattern);
1469
+ if (index === void 0) {
1470
+ return NOT_ROUTER;
1471
+ }
1472
+ const construction = index.constructions.get(module)?.get(objectName);
1473
+ if (construction === void 0) {
1474
+ return declaredOnMountObject(index, module, objectName);
1475
+ }
1476
+ if (construction.reassigned) {
1477
+ return {
1478
+ kind: "abstain",
1479
+ reason: "shares its variable name with a second router construction"
1480
+ };
1481
+ }
1482
+ const ownPrefix = composedOwnPrefix(
1483
+ construction.prefix,
1484
+ index.composition
1485
+ );
1486
+ if (ownPrefix.kind === "abstain") {
1487
+ return ownPrefix;
1488
+ }
1489
+ const mount = index.mounts.get(construction);
1490
+ if (mount === void 0) {
1491
+ return { kind: "abstain", reason: unmountedReason(index) };
1492
+ }
1493
+ if (mount.kind === "abstain") {
1494
+ return { kind: "abstain", reason: mount.reason };
1495
+ }
1496
+ const rivalled = rivalRegistration(index, mount.site);
1497
+ if (rivalled !== null) {
1498
+ return { kind: "abstain", reason: rivalled };
1499
+ }
1500
+ return {
1501
+ kind: "composed",
1502
+ value: mount.includePrefix + ownPrefix.value
1503
+ };
1504
+ }
1505
+ };
1506
+ }
1507
+ function declaredOnMountObject(index, module, objectName) {
1508
+ const objectPrefix = index.objectPrefixes.get(module)?.get(objectName);
1509
+ if (objectPrefix === void 0) {
1510
+ return NOT_ROUTER;
1511
+ }
1512
+ if (objectPrefix.kind === "composed" && objectPrefix.value === "") {
1513
+ return NOT_ROUTER;
1514
+ }
1515
+ return objectPrefix;
1516
+ }
1517
+ function sameSite(one, other) {
1518
+ if (one.kind === "function" && other.kind === "function") {
1519
+ return one.node === other.node;
1520
+ }
1521
+ return one.kind === other.kind;
1522
+ }
1523
+ function loopsClause(loops, verb) {
1524
+ const locations = loops.map((loop) => loop.location).sort();
1525
+ if (locations.length === 1) {
1526
+ return `a loop at ${locations[0]} ${verb}s`;
1527
+ }
1528
+ return `loops at ${locations.join(", ")} ${verb}`;
1529
+ }
1530
+ function unmountedReason(index) {
1531
+ const loops = [...index.unenumerableLoops.values()];
1532
+ if (loops.length === 0) {
1533
+ return "is never mounted through a single variable binding in the files read";
1534
+ }
1535
+ return `is not mounted by name in the files read, and ${loopsClause(loops, "mount")} routers read out of a call this reading does not follow`;
1536
+ }
1537
+ function rivalRegistration(index, site) {
1538
+ if (site.kind === "module") {
1539
+ return null;
1540
+ }
1541
+ const rivals = [...index.unenumerableLoops.values()].filter(
1542
+ (loop) => !sameSite(loop.site, site)
1543
+ );
1544
+ if (rivals.length === 0) {
1545
+ return null;
1546
+ }
1547
+ return `is mounted inside a function, while ${loopsClause(rivals, "mount")} routers this reading cannot name, and which of them the app runs is not written down`;
1548
+ }
1549
+ function composedOwnPrefix(prefix, composition) {
1550
+ const readings = {
1551
+ stated: (reading) => ({
1552
+ kind: "composed",
1553
+ value: reading.kind === "stated" ? reading.value : ""
1554
+ }),
1555
+ unreadable: () => ({
1556
+ kind: "abstain",
1557
+ reason: "declares a prefix that is not a string literal"
1558
+ }),
1559
+ unstated: () => {
1560
+ if (composition.constructorPrefixRequired === true) {
1561
+ return {
1562
+ kind: "abstain",
1563
+ reason: "states no prefix where it is constructed, and its library derives one from elsewhere"
1564
+ };
1565
+ }
1566
+ return { kind: "composed", value: "" };
1567
+ }
1568
+ };
1569
+ return readings[prefix.kind](prefix);
1570
+ }
1571
+ function buildPatternIndex(files, importModule, composition, resolverOptions) {
1572
+ const index = {
1573
+ composition,
1574
+ constructions: /* @__PURE__ */ new Map(),
1575
+ mounts: /* @__PURE__ */ new Map(),
1576
+ unenumerableLoops: /* @__PURE__ */ new Map(),
1577
+ carriers: /* @__PURE__ */ new Map(),
1578
+ carrierRegistrations: /* @__PURE__ */ new Map(),
1579
+ handoffs: /* @__PURE__ */ new Map(),
1580
+ objectPrefixes: /* @__PURE__ */ new Map()
1581
+ };
1582
+ const byFile = new Map(files.map((bound) => [bound.file, bound]));
1583
+ const scanOf = (bound) => ({
1584
+ bound,
1585
+ byFile,
1586
+ importModule,
1587
+ composition,
1588
+ resolverOptions,
1589
+ index
1590
+ });
1591
+ const objectPrefix = composition.mountObjectPrefix;
1592
+ for (const bound of files) {
1593
+ collectConstructions(bound, importModule, composition, index);
1594
+ }
1595
+ if (objectPrefix?.carrier !== void 0) {
1596
+ const carrier = objectPrefix.carrier;
1597
+ for (const bound of files) {
1598
+ collectCarrierConstructions(carrier, composition, scanOf(bound));
1599
+ }
1600
+ for (const bound of files) {
1601
+ collectCarrierCalls(carrier, scanOf(bound));
1602
+ }
1603
+ }
1604
+ if (objectPrefix !== void 0) {
1605
+ for (const bound of files) {
1606
+ collectObjectPrefixes(objectPrefix, scanOf(bound));
1607
+ }
1608
+ }
1609
+ for (const bound of files) {
1610
+ collectMounts(
1611
+ bound,
1612
+ byFile,
1613
+ importModule,
1614
+ composition,
1615
+ resolverOptions,
1616
+ index
1617
+ );
1618
+ }
1619
+ return index;
1620
+ }
1621
+ function constructionOf(name, scope, importModule) {
1622
+ const binding = resolveName(scope, name);
1623
+ if (binding?.kind !== "assignment" || binding.value?.type !== "call") {
1624
+ return null;
1625
+ }
1626
+ const callee = field(binding.value, "function");
1627
+ if (callee?.type !== "identifier") {
1628
+ return null;
1629
+ }
1630
+ const calleeBinding = resolveName(scope, callee.text);
1631
+ if (calleeBinding?.kind !== "importFrom" || !importModule.includes(calleeBinding.module)) {
1632
+ return null;
1633
+ }
1634
+ return { constructorName: calleeBinding.importedName, call: binding.value };
1635
+ }
1636
+ var PREFIX_TRAILING_SLASH_READERS = {
1637
+ kept: (prefix) => prefix,
1638
+ trimmed: (prefix) => prefix.replace(/\/+$/, "")
1639
+ };
1640
+ var NO_VALUE_LITERALS = {
1641
+ string: (arg) => arg.kind === "string" && arg.value === "",
1642
+ number: (arg) => arg.kind === "number" && arg.value === 0,
1643
+ boolean: (arg) => arg.kind === "boolean" && !arg.value,
1644
+ none: () => true
1645
+ };
1646
+ function readPrefixKeyword(keywordArgs, keyword, composition) {
1647
+ const arg = keywordArgs[keyword];
1648
+ if (arg === void 0) {
1649
+ return UNSTATED_PREFIX;
1650
+ }
1651
+ if ((composition.noValuePrefix ?? "unreadable") === "unstated" && NO_VALUE_LITERALS[arg.kind]?.(arg) === true) {
1652
+ return UNSTATED_PREFIX;
1653
+ }
1654
+ if (arg.kind === "string") {
1655
+ return { kind: "stated", value: arg.value };
1656
+ }
1657
+ return UNREADABLE_PREFIX;
1658
+ }
1659
+ function constructorPrefix(keywordArgs, composition) {
1660
+ const reading = readPrefixKeyword(
1661
+ keywordArgs,
1662
+ composition.prefixKeyword,
1663
+ composition
1664
+ );
1665
+ if (reading.kind !== "stated") {
1666
+ return reading;
1667
+ }
1668
+ const trailingSlash = composition.constructorPrefixTrailingSlash ?? "kept";
1669
+ return {
1670
+ kind: "stated",
1671
+ value: PREFIX_TRAILING_SLASH_READERS[trailingSlash](reading.value)
1672
+ };
1673
+ }
1674
+ function constructionStatement(stmt, scope, importModule) {
1675
+ if (stmt.type !== "expression_statement") {
1676
+ return null;
1677
+ }
1678
+ const assignment = stmt.namedChildren.find(
1679
+ (child) => child !== null && child.type === "assignment"
1680
+ );
1681
+ if (assignment === void 0) {
1682
+ return null;
1683
+ }
1684
+ const left = field(assignment, "left");
1685
+ const right = field(assignment, "right");
1686
+ if (left?.type !== "identifier" || right?.type !== "call") {
1687
+ return null;
1688
+ }
1689
+ const callee = field(right, "function");
1690
+ if (callee?.type !== "identifier") {
1691
+ return null;
1692
+ }
1693
+ const calleeBinding = resolveName(scope, callee.text);
1694
+ if (calleeBinding?.kind !== "importFrom" || !importModule.includes(calleeBinding.module)) {
1695
+ return null;
1696
+ }
1697
+ return {
1698
+ name: left.text,
1699
+ constructorName: calleeBinding.importedName,
1700
+ call: right
1701
+ };
1702
+ }
1703
+ function recordConstruction(construction, module, prefixOf, into) {
1704
+ const perModule = into.get(module) ?? /* @__PURE__ */ new Map();
1705
+ into.set(module, perModule);
1706
+ const existing = perModule.get(construction.name);
1707
+ if (existing !== void 0) {
1708
+ existing.reassigned = true;
1709
+ return;
1710
+ }
1711
+ const { keywordArgs } = readCallArguments(
1712
+ field(construction.call, "arguments")
1713
+ );
1714
+ perModule.set(construction.name, {
1715
+ constructorName: construction.constructorName,
1716
+ prefix: prefixOf(keywordArgs),
1717
+ reassigned: false
1718
+ });
1719
+ }
1720
+ function collectConstructions(bound, importModule, composition, index) {
1721
+ for (const stmt of bodyStatements(bound.root)) {
1722
+ const construction = constructionStatement(
1723
+ stmt,
1724
+ bound.module.moduleScope,
1725
+ importModule
1726
+ );
1727
+ if (construction === null || construction.constructorName !== composition.routerConstructorName) {
1728
+ continue;
1729
+ }
1730
+ recordConstruction(
1731
+ construction,
1732
+ bound.module,
1733
+ (keywordArgs) => constructorPrefix(keywordArgs, composition),
1734
+ index.constructions
1735
+ );
1736
+ }
1737
+ }
1738
+ function collectCarrierConstructions(carrier, composition, scan) {
1739
+ walkStatements(
1740
+ bodyStatements(scan.bound.root),
1741
+ modulePosition(scan),
1742
+ scan,
1743
+ (stmt, position) => {
1744
+ const construction = constructionStatement(
1745
+ stmt,
1746
+ position.scope,
1747
+ carrier.importModule
1748
+ );
1749
+ if (construction === null) {
1750
+ return;
1751
+ }
1752
+ recordConstruction(
1753
+ construction,
1754
+ scan.bound.module,
1755
+ (keywordArgs) => readPrefixKeyword(keywordArgs, carrier.prefixKeyword, composition),
1756
+ scan.index.carriers
1757
+ );
1758
+ }
1759
+ );
1760
+ }
1761
+ function constructionNamed(name, scope, scan, constructions = scan.index.constructions) {
1762
+ const binding = resolveName(scope, name);
1763
+ if (binding === null) {
1764
+ return null;
1765
+ }
1766
+ const resolvers = {
1767
+ assignment: () => constructions.get(scan.bound.module)?.get(name) ?? null,
1768
+ importFrom: () => {
1769
+ if (binding.kind !== "importFrom") {
1770
+ return null;
1771
+ }
1772
+ const resolution = resolveModule(
1773
+ scan.bound.file,
1774
+ { module: binding.module, relativeLevel: binding.relativeLevel },
1775
+ scan.resolverOptions
1776
+ );
1777
+ if (resolution.status !== "resolved") {
1778
+ return null;
1779
+ }
1780
+ const target = scan.byFile.get(resolution.file);
1781
+ if (target === void 0) {
1782
+ return null;
1783
+ }
1784
+ return constructions.get(target.module)?.get(binding.importedName) ?? null;
1785
+ }
1786
+ };
1787
+ return resolvers[binding.kind]?.() ?? null;
1788
+ }
1789
+ var MOUNT_STATE_BY_EFFECT = {
1790
+ prefixes: (statedPrefix, site) => ({
1791
+ kind: "mounted",
1792
+ includePrefix: statedPrefix,
1793
+ site
1794
+ }),
1795
+ replaces: () => ({
1796
+ kind: "abstain",
1797
+ reason: "is mounted under a prefix that replaces the one it was constructed with, which this reading does not follow"
1798
+ })
1799
+ };
1800
+ function recordMount(index, target, state) {
1801
+ if (index.mounts.has(target)) {
1802
+ index.mounts.set(target, {
1803
+ kind: "abstain",
1804
+ reason: "is mounted more than once"
1805
+ });
1806
+ return;
1807
+ }
1808
+ index.mounts.set(target, state);
1809
+ }
1810
+ function collectMounts(bound, byFile, importModule, composition, resolverOptions, index) {
1811
+ const scan = {
1812
+ bound,
1813
+ byFile,
1814
+ importModule,
1815
+ composition,
1816
+ resolverOptions,
1817
+ index
1818
+ };
1819
+ walkStatements(
1820
+ bodyStatements(bound.root),
1821
+ modulePosition(scan),
1822
+ scan,
1823
+ recordMountStatement
1824
+ );
1825
+ }
1826
+ function modulePosition(scan) {
1827
+ return {
1828
+ scope: scan.bound.module.moduleScope,
1829
+ loopBindings: /* @__PURE__ */ new Map(),
1830
+ site: MODULE_SITE
1831
+ };
1832
+ }
1833
+ var WALK_DESCENTS = {
1834
+ decorated_definition: (stmt, position, scan, visit) => {
1835
+ walkStatements([stripDecorators(stmt).definition], position, scan, visit);
1836
+ },
1837
+ function_definition: (stmt, position, scan, visit) => {
1838
+ const scope = scan.bound.module.scopeFor.get(stmt.id);
1839
+ if (scope === void 0) {
1840
+ return;
1841
+ }
1842
+ walkStatements(
1843
+ nestedStatements(stmt),
1844
+ {
1845
+ scope,
1846
+ loopBindings: /* @__PURE__ */ new Map(),
1847
+ site: { kind: "function", node: stmt.id }
1848
+ },
1849
+ scan,
1850
+ visit
1851
+ );
1852
+ },
1853
+ for_statement: (stmt, position, scan, visit) => {
1854
+ walkStatements(
1855
+ nestedStatements(stmt),
1856
+ { ...position, loopBindings: loopBindingsOf(stmt, position, scan) },
1857
+ scan,
1858
+ visit
1859
+ );
1860
+ }
1861
+ };
1862
+ function nestedStatements(stmt) {
1863
+ const body = field(stmt, "body");
1864
+ return body === null ? [] : bodyStatements(body);
1865
+ }
1866
+ function walkStatements(statements, position, scan, visit) {
1867
+ for (const stmt of statements) {
1868
+ const descend = WALK_DESCENTS[stmt.type];
1869
+ if (descend !== void 0) {
1870
+ descend(stmt, position, scan, visit);
1871
+ continue;
1872
+ }
1873
+ visit(stmt, position, scan);
1874
+ }
1875
+ }
1876
+ function targetNames(node) {
1877
+ if (node.type === "identifier") {
1878
+ return [node.text];
1879
+ }
1880
+ return bodyStatements(node).flatMap(targetNames);
1881
+ }
1882
+ function sequenceElementNames(node) {
1883
+ if (node === null || node.type !== "list" && node.type !== "tuple") {
1884
+ return null;
1885
+ }
1886
+ const names = [];
1887
+ for (const child of node.namedChildren) {
1888
+ if (child?.type !== "identifier") {
1889
+ return null;
1890
+ }
1891
+ names.push(child.text);
1892
+ }
1893
+ return names;
1894
+ }
1895
+ function loopBindingsOf(stmt, position, scan) {
1896
+ const bindings = new Map(position.loopBindings);
1897
+ const left = field(stmt, "left");
1898
+ if (left === null) {
1899
+ return bindings;
1900
+ }
1901
+ const elements = sequenceElementNames(field(stmt, "right"));
1902
+ if (left.type === "identifier" && elements !== null) {
1903
+ bindings.set(left.text, { kind: "sequence", elements });
1904
+ return bindings;
1905
+ }
1906
+ const location = `${scan.bound.displayPath}:${rangeOf(stmt).start}`;
1907
+ for (const name of targetNames(left)) {
1908
+ bindings.set(name, { kind: "unenumerable", location });
1909
+ }
1910
+ return bindings;
1911
+ }
1912
+ function mountCallOf(stmt, scope, scan) {
1913
+ if (stmt.type !== "expression_statement") {
1914
+ return null;
1915
+ }
1916
+ const call = stmt.namedChild(0);
1917
+ if (call === null || call.type !== "call") {
1918
+ return null;
1919
+ }
1920
+ const callee = field(call, "function");
1921
+ if (callee === null || callee.type !== "attribute") {
1922
+ return null;
1923
+ }
1924
+ const objectNode = field(callee, "object");
1925
+ const attributeNode = field(callee, "attribute");
1926
+ if (objectNode?.type !== "identifier" || attributeNode?.text !== scan.composition.includeMethodName) {
1927
+ return null;
1928
+ }
1929
+ const includer = constructionOf(objectNode.text, scope, scan.importModule);
1930
+ if (includer === null) {
1931
+ return null;
1932
+ }
1933
+ return {
1934
+ call,
1935
+ objectName: objectNode.text,
1936
+ includerConstructorName: includer.constructorName,
1937
+ includerCall: includer.call
1938
+ };
1939
+ }
1940
+ function mountedNames(arg, position, index) {
1941
+ if (arg?.kind !== "identifier") {
1942
+ return [];
1943
+ }
1944
+ const target = position.loopBindings.get(arg.name);
1945
+ if (target === void 0) {
1946
+ return [arg.name];
1947
+ }
1948
+ if (target.kind === "sequence") {
1949
+ return target.elements;
1950
+ }
1951
+ index.unenumerableLoops.set(target.location, {
1952
+ site: position.site,
1953
+ location: target.location
1954
+ });
1955
+ return [];
1956
+ }
1957
+ function recordMountStatement(stmt, position, scan) {
1958
+ const mountCall = mountCallOf(stmt, position.scope, scan);
1959
+ if (mountCall === null) {
1960
+ return;
1961
+ }
1962
+ const { args, keywordArgs } = readCallArguments(
1963
+ field(mountCall.call, "arguments")
1964
+ );
1965
+ const objectPrefix = mountObjectPrefix(
1966
+ mountCall.objectName,
1967
+ mountCall.includerCall,
1968
+ position.scope,
1969
+ scan
1970
+ );
1971
+ const state = mountStateOf(
1972
+ mountCall.includerConstructorName,
1973
+ keywordArgs,
1974
+ objectPrefix,
1975
+ scan.composition,
1976
+ position.site
1977
+ );
1978
+ for (const name of mountedNames(args[0], position, scan.index)) {
1979
+ const target = constructionNamed(name, position.scope, scan);
1980
+ if (target !== null) {
1981
+ recordMount(scan.index, target, state);
1982
+ }
1983
+ }
1984
+ }
1985
+ function mountStateOf(includerConstructorName, keywordArgs, objectPrefix, composition, site) {
1986
+ if (includerConstructorName === composition.routerConstructorName) {
1987
+ return {
1988
+ kind: "abstain",
1989
+ reason: "is mounted onto another router, one hop past what this reading follows"
1990
+ };
1991
+ }
1992
+ if (objectPrefix.kind === "abstain") {
1993
+ return {
1994
+ kind: "abstain",
1995
+ reason: `is mounted on an object that ${objectPrefix.reason}`
1996
+ };
1997
+ }
1998
+ const mountPrefix = readPrefixKeyword(
1999
+ keywordArgs,
2000
+ composition.prefixKeyword,
2001
+ composition
2002
+ );
2003
+ if (mountPrefix.kind === "unreadable") {
2004
+ return {
2005
+ kind: "abstain",
2006
+ reason: "is mounted with a prefix that is not a string literal"
2007
+ };
2008
+ }
2009
+ if (mountPrefix.kind === "unstated") {
2010
+ return { kind: "mounted", includePrefix: objectPrefix.value, site };
2011
+ }
2012
+ const effect = composition.mountPrefixEffect ?? "prefixes";
2013
+ const state = MOUNT_STATE_BY_EFFECT[effect](mountPrefix.value, site);
2014
+ if (state.kind === "abstain") {
2015
+ return state;
2016
+ }
2017
+ return { ...state, includePrefix: objectPrefix.value + state.includePrefix };
2018
+ }
2019
+ function collectCarrierCalls(carrier, scan) {
2020
+ walkStatements(
2021
+ bodyStatements(scan.bound.root),
2022
+ modulePosition(scan),
2023
+ scan,
2024
+ (stmt, position) => {
2025
+ const call = attributeCallOf(stmt);
2026
+ if (call === null) {
2027
+ return;
2028
+ }
2029
+ if (call.attribute === carrier.handoffMethodName) {
2030
+ recordHandoff(call, scan);
2031
+ return;
2032
+ }
2033
+ if (call.attribute === carrier.registerMethodName) {
2034
+ recordRegistrationCall(call, carrier, position, scan);
2035
+ }
2036
+ }
2037
+ );
2038
+ }
2039
+ function attributeCallOf(stmt) {
2040
+ if (stmt.type !== "expression_statement") {
2041
+ return null;
2042
+ }
2043
+ const call = stmt.namedChild(0);
2044
+ if (call === null || call.type !== "call") {
2045
+ return null;
2046
+ }
2047
+ const callee = field(call, "function");
2048
+ if (callee === null || callee.type !== "attribute") {
2049
+ return null;
2050
+ }
2051
+ const objectNode = field(callee, "object");
2052
+ const attributeNode = field(callee, "attribute");
2053
+ if (objectNode?.type !== "identifier" || attributeNode === null) {
2054
+ return null;
2055
+ }
2056
+ return {
2057
+ objectName: objectNode.text,
2058
+ attribute: attributeNode.text,
2059
+ ...readCallArguments(field(call, "arguments"))
2060
+ };
2061
+ }
2062
+ function recordHandoff(call, scan) {
2063
+ const perModule = scan.index.handoffs.get(scan.bound.module) ?? /* @__PURE__ */ new Map();
2064
+ scan.index.handoffs.set(scan.bound.module, perModule);
2065
+ const handed = perModule.get(call.objectName) ?? [];
2066
+ if (call.args[0] !== void 0) {
2067
+ handed.push(call.args[0]);
2068
+ }
2069
+ perModule.set(call.objectName, handed);
2070
+ }
2071
+ function recordRegistrationCall(call, carrier, position, scan) {
2072
+ const arg = call.args[0];
2073
+ if (arg?.kind !== "identifier" || position.loopBindings.has(arg.name)) {
2074
+ return;
2075
+ }
2076
+ const target = constructionNamed(
2077
+ arg.name,
2078
+ position.scope,
2079
+ scan,
2080
+ scan.index.carriers
2081
+ );
2082
+ if (target === null || target.constructorName !== carrier.constructorName) {
2083
+ return;
2084
+ }
2085
+ recordRegistration(
2086
+ scan.index,
2087
+ target,
2088
+ registrationState(call, carrier, position, scan)
2089
+ );
2090
+ }
2091
+ function registrationState(call, carrier, position, scan) {
2092
+ if (call.keywordArgs[carrier.prefixKeyword] !== void 0) {
2093
+ return {
2094
+ kind: "abstain",
2095
+ reason: "is built from one registered under a prefix stated where it is registered"
2096
+ };
2097
+ }
2098
+ const host = constructionNamed(
2099
+ call.objectName,
2100
+ position.scope,
2101
+ scan,
2102
+ scan.index.carriers
2103
+ );
2104
+ if (host !== null && host.constructorName === carrier.constructorName) {
2105
+ return {
2106
+ kind: "abstain",
2107
+ reason: "is built from one registered inside another, one hop past what this reading follows"
2108
+ };
2109
+ }
2110
+ return { kind: "asBuilt" };
2111
+ }
2112
+ function recordRegistration(index, target, state) {
2113
+ if (index.carrierRegistrations.has(target)) {
2114
+ index.carrierRegistrations.set(target, {
2115
+ kind: "abstain",
2116
+ reason: "is built from one registered more than once"
2117
+ });
2118
+ return;
2119
+ }
2120
+ index.carrierRegistrations.set(target, state);
2121
+ }
2122
+ function collectObjectPrefixes(spec, scan) {
2123
+ for (const stmt of bodyStatements(scan.bound.root)) {
2124
+ const construction = constructionStatement(
2125
+ stmt,
2126
+ scan.bound.module.moduleScope,
2127
+ scan.importModule
2128
+ );
2129
+ if (construction === null || construction.constructorName === scan.composition.routerConstructorName) {
2130
+ continue;
2131
+ }
2132
+ const perModule = scan.index.objectPrefixes.get(scan.bound.module) ?? /* @__PURE__ */ new Map();
2133
+ scan.index.objectPrefixes.set(scan.bound.module, perModule);
2134
+ perModule.set(
2135
+ construction.name,
2136
+ readMountObjectPrefix(
2137
+ spec,
2138
+ construction.name,
2139
+ construction.call,
2140
+ scan.bound.module.moduleScope,
2141
+ scan
2142
+ )
2143
+ );
2144
+ }
2145
+ }
2146
+ function mountObjectPrefix(objectName, constructionCall, scope, scan) {
2147
+ const spec = scan.composition.mountObjectPrefix;
2148
+ if (spec === void 0) {
2149
+ return { kind: "composed", value: "" };
2150
+ }
2151
+ return readMountObjectPrefix(spec, objectName, constructionCall, scope, scan);
2152
+ }
2153
+ function readMountObjectPrefix(spec, objectName, constructionCall, scope, scan) {
2154
+ const { args, keywordArgs } = readCallArguments(
2155
+ field(constructionCall, "arguments")
2156
+ );
2157
+ const own = spec.prefixKeyword === void 0 ? UNSTATED_PREFIX : readPrefixKeyword(keywordArgs, spec.prefixKeyword, scan.composition);
2158
+ if (own.kind === "unreadable") {
2159
+ return {
2160
+ kind: "abstain",
2161
+ reason: "states a prefix of its own that is not a string literal"
2162
+ };
2163
+ }
2164
+ const carried = spec.carrier === void 0 ? { kind: "composed", value: "" } : carrierPrefix(
2165
+ spec.carrier,
2166
+ args[spec.carrier.argumentIndex],
2167
+ objectName,
2168
+ scope,
2169
+ scan
2170
+ );
2171
+ if (carried.kind === "abstain") {
2172
+ return carried;
2173
+ }
2174
+ return {
2175
+ kind: "composed",
2176
+ value: carried.value + (own.kind === "stated" ? own.value : "")
2177
+ };
2178
+ }
2179
+ function carrierPrefix(carrier, constructorArg, objectName, scope, scan) {
2180
+ const handed = scan.index.handoffs.get(scan.bound.module)?.get(objectName) ?? [];
2181
+ const candidates = [
2182
+ ...constructorArg === void 0 ? [] : [constructorArg],
2183
+ ...handed
2184
+ ];
2185
+ if (candidates.length > 1) {
2186
+ return { kind: "abstain", reason: "is built from more than one candidate" };
2187
+ }
2188
+ const candidate = candidates[0];
2189
+ if (candidate === void 0) {
2190
+ return { kind: "composed", value: "" };
2191
+ }
2192
+ if (candidate.kind !== "identifier") {
2193
+ return {
2194
+ kind: "abstain",
2195
+ reason: "is built from something this reading cannot follow"
2196
+ };
2197
+ }
2198
+ const construction = constructionNamed(
2199
+ candidate.name,
2200
+ scope,
2201
+ scan,
2202
+ scan.index.carriers
2203
+ );
2204
+ if (construction === null) {
2205
+ return {
2206
+ kind: "abstain",
2207
+ reason: "is built from something this reading cannot follow"
2208
+ };
2209
+ }
2210
+ if (construction.constructorName !== carrier.constructorName) {
2211
+ return { kind: "composed", value: "" };
2212
+ }
2213
+ if (construction.reassigned) {
2214
+ return {
2215
+ kind: "abstain",
2216
+ reason: "is built from one whose variable name holds a second construction"
2217
+ };
2218
+ }
2219
+ const registration = scan.index.carrierRegistrations.get(construction);
2220
+ if (registration?.kind === "abstain") {
2221
+ return { kind: "abstain", reason: registration.reason };
2222
+ }
2223
+ const readings = {
2224
+ stated: () => ({
2225
+ kind: "composed",
2226
+ value: construction.prefix.kind === "stated" ? construction.prefix.value : ""
2227
+ }),
2228
+ unstated: () => ({ kind: "composed", value: "" }),
2229
+ unreadable: () => ({
2230
+ kind: "abstain",
2231
+ reason: "is built from one whose prefix is not a string literal"
2232
+ })
2233
+ };
2234
+ return readings[construction.prefix.kind]();
2235
+ }
2236
+
2237
+ // src/project.ts
2238
+ async function extractPythonProject(options) {
2239
+ const db = new Database();
2240
+ const summaries = [];
2241
+ const gapHandling = options.gapHandling ?? "permissive";
2242
+ const displayPathOf = (file) => options.workspaceRoot !== void 0 ? path3.relative(options.workspaceRoot, file) : file;
2243
+ const bound = [];
2244
+ for (const file of options.files) {
2245
+ const source = fs3.readFileSync(file, "utf8");
2246
+ const tree = await parsePython(source);
2247
+ bound.push({
2248
+ file,
2249
+ displayPath: displayPathOf(file),
2250
+ root: tree.rootNode,
2251
+ module: bindModule(tree.rootNode)
2252
+ });
2253
+ }
2254
+ const routerIndex = buildRouterIndex(bound, options.packs, {
2255
+ roots: options.roots
2256
+ });
2257
+ for (const { file, root, module: moduleBinding } of bound) {
2258
+ const displayPath = displayPathOf(file);
2259
+ const rawUnits = discoverUnits(root, moduleBinding, {
2260
+ packs: options.packs,
2261
+ filePath: displayPath,
2262
+ routerIndex,
2263
+ gapHandling
2264
+ });
2265
+ for (const raw of rawUnits) {
2266
+ const summary = assembleSummary(raw, { gapHandling });
2267
+ summary.confidence = { source: "inferred_static", level: "low" };
2268
+ summaries.push(summary);
2269
+ emitEntryFact(db, file, raw.identity.range, raw.identity.name);
2270
+ }
2271
+ emitModuleImportFacts(db, file, moduleBinding, { roots: options.roots });
2272
+ }
2273
+ return { summaries, facts: db };
2274
+ }
2275
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
2276
+ "__pycache__",
2277
+ ".venv",
2278
+ "venv",
2279
+ "node_modules",
2280
+ ".git"
2281
+ ]);
2282
+ function findPythonFiles(root) {
2283
+ const found = [];
2284
+ const walk = (dir) => {
2285
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
2286
+ if (entry.isDirectory()) {
2287
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
2288
+ walk(path3.join(dir, entry.name));
2289
+ }
2290
+ continue;
2291
+ }
2292
+ if (entry.isFile() && entry.name.endsWith(".py")) {
2293
+ found.push(path3.join(dir, entry.name));
2294
+ }
2295
+ }
2296
+ };
2297
+ walk(root);
2298
+ return found.sort();
2299
+ }
2300
+ export {
2301
+ annotationToShape,
2302
+ bindModule,
2303
+ buildRouterIndex,
2304
+ classifyDecorator,
2305
+ discoverUnits,
2306
+ emitEntryFact,
2307
+ emitModuleImportFacts,
2308
+ extractPythonProject,
2309
+ findPythonFiles,
2310
+ parsePython,
2311
+ resolveAbsoluteModule,
2312
+ resolveModule,
2313
+ resolveName,
2314
+ resolveRelativeModule,
2315
+ shapeFromName,
2316
+ unitKey
2317
+ };