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