@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/LICENSE +205 -0
- package/README.md +166 -0
- package/dist/index.cjs +2364 -0
- package/dist/index.d.cts +459 -0
- package/dist/index.d.ts +459 -0
- package/dist/index.js +2317 -0
- package/grammar/NOTICE +28 -0
- package/grammar/README.md +37 -0
- package/grammar/tree-sitter-python.wasm +0 -0
- package/package.json +64 -0
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
|
+
};
|