@orangepro/orangepro-mcp 0.2.42 → 0.2.44
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/local/analyze/analyzer.js +283 -18
- package/dist/local/analyze/treeSitter/engine.js +604 -8
- package/dist/local/autoProve.js +222 -48
- package/dist/local/graph/ontology.js +3 -3
- package/dist/local/proofDoctor.js +8 -3
- package/dist/local/provenance.js +1 -1
- package/dist/local/score/risk.js +3 -2
- package/package.json +2 -2
|
@@ -8,6 +8,8 @@ import { createRequire } from "node:module";
|
|
|
8
8
|
import { Parser, Language } from "web-tree-sitter";
|
|
9
9
|
import { MAX_SYMBOLS_PER_FILE } from "../symbols.js";
|
|
10
10
|
import { TS_LANG_CONFIGS } from "./languages.js";
|
|
11
|
+
import { slugify } from "../../util/ids.js";
|
|
12
|
+
import { shortHash } from "../../util/hash.js";
|
|
11
13
|
const require = createRequire(import.meta.url);
|
|
12
14
|
let initPromise = null;
|
|
13
15
|
const loaded = new Map();
|
|
@@ -212,6 +214,39 @@ function symbolName(node, cfg) {
|
|
|
212
214
|
function nodeLines(node) {
|
|
213
215
|
return { start_line: node.startPosition.row + 1, end_line: node.endPosition.row + 1 };
|
|
214
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Nearest lexical Python class for a function. A function boundary reached first
|
|
219
|
+
* means this is a nested local function, not a class method.
|
|
220
|
+
*/
|
|
221
|
+
function pythonOwnerClass(node) {
|
|
222
|
+
const owners = [];
|
|
223
|
+
let parent = node.parent;
|
|
224
|
+
while (parent) {
|
|
225
|
+
if (parent.type === "function_definition" || parent.type === "lambda")
|
|
226
|
+
return undefined;
|
|
227
|
+
if (parent.type === "class_definition") {
|
|
228
|
+
const name = parent.childForFieldName("name")?.text;
|
|
229
|
+
if (name)
|
|
230
|
+
owners.unshift(name);
|
|
231
|
+
}
|
|
232
|
+
parent = parent.parent;
|
|
233
|
+
}
|
|
234
|
+
return owners.length > 0 ? owners.join(".") : undefined;
|
|
235
|
+
}
|
|
236
|
+
function hasPythonFunctionAncestor(node) {
|
|
237
|
+
let parent = node.parent;
|
|
238
|
+
while (parent) {
|
|
239
|
+
if (parent.type === "function_definition" || parent.type === "lambda")
|
|
240
|
+
return true;
|
|
241
|
+
parent = parent.parent;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
function pythonQualifiedFunctionName(node) {
|
|
246
|
+
const name = functionName(node, "python");
|
|
247
|
+
const owner = pythonOwnerClass(node);
|
|
248
|
+
return name && owner ? `${owner}.${name}` : name;
|
|
249
|
+
}
|
|
215
250
|
/**
|
|
216
251
|
* Extract class/function/method symbol metadata (names + a trivial-accessor flag —
|
|
217
252
|
* never source bodies) from a tree-sitter-supported language. Sync; returns [] when
|
|
@@ -249,16 +284,22 @@ export function extractTreeSitterSymbols(content, language) {
|
|
|
249
284
|
kind = "method";
|
|
250
285
|
else if (cfg.functionTypes.has(node.type))
|
|
251
286
|
kind = "function";
|
|
252
|
-
|
|
287
|
+
const pythonOwner = language === "python" && node.type === "function_definition" ? pythonOwnerClass(node) : undefined;
|
|
288
|
+
if (pythonOwner && kind === "function")
|
|
289
|
+
kind = "method";
|
|
290
|
+
const localPythonDeclaration = language === "python" && (node.type === "function_definition" || node.type === "class_definition") && hasPythonFunctionAncestor(node);
|
|
291
|
+
if (kind && !localPythonDeclaration && shouldEmitSymbol(node, language)) {
|
|
253
292
|
const name = symbolName(node, cfg);
|
|
254
293
|
if (name && !RESERVED_SYMBOL_NAMES.has(name)) {
|
|
255
294
|
// Go methods mint receiver-qualified names (`Recv.M`, mirroring TS/JS
|
|
256
295
|
// `Class.method` + member_of) so same-named methods on different receivers
|
|
257
296
|
// stay distinct symbols; an underivable receiver falls back to the bare name.
|
|
258
|
-
const recv = kind === "method" && language === "go" ? goReceiverBaseName(node) : undefined;
|
|
297
|
+
const recv = kind === "method" && language === "go" ? goReceiverBaseName(node) : language === "python" ? pythonOwner : undefined;
|
|
298
|
+
const pythonClassOwner = kind === "class" && language === "python" ? pythonOwnerClass(node) : undefined;
|
|
299
|
+
const emittedName = pythonClassOwner ? `${pythonClassOwner}.${name}` : name;
|
|
259
300
|
add(kind === "method"
|
|
260
301
|
? { name: recv ? `${recv}.${name}` : name, ...(recv ? { member_of: recv } : {}), symbol_kind: kind, trivial_accessor: isTrivialAccessorBody(node, language), ...nodeLines(node) }
|
|
261
|
-
: { name, symbol_kind: kind, ...nodeLines(node) });
|
|
302
|
+
: { name: emittedName, symbol_kind: kind, ...nodeLines(node) });
|
|
262
303
|
}
|
|
263
304
|
}
|
|
264
305
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -464,6 +505,21 @@ function kotlinTopLevelSymbols(root) {
|
|
|
464
505
|
}
|
|
465
506
|
return [...out].sort();
|
|
466
507
|
}
|
|
508
|
+
function underPythonTypeCheckingGuard(node) {
|
|
509
|
+
let parent = node.parent;
|
|
510
|
+
while (parent) {
|
|
511
|
+
if (parent.type === "if_statement") {
|
|
512
|
+
const condition = parent.childForFieldName("condition");
|
|
513
|
+
const text = condition?.text.trim() ?? "";
|
|
514
|
+
if (text === "TYPE_CHECKING" || /^[A-Za-z_]\w*\.TYPE_CHECKING$/.test(text))
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
if (parent.type === "function_definition" || parent.type === "class_definition")
|
|
518
|
+
break;
|
|
519
|
+
parent = parent.parent;
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
467
523
|
function extractImports(root, language) {
|
|
468
524
|
const imports = [];
|
|
469
525
|
const add = (binding) => {
|
|
@@ -472,6 +528,9 @@ function extractImports(root, language) {
|
|
|
472
528
|
imports.push(binding);
|
|
473
529
|
};
|
|
474
530
|
const walk = (node) => {
|
|
531
|
+
if (language === "python" && (node.type === "import_statement" || node.type === "import_from_statement") && underPythonTypeCheckingGuard(node)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
475
534
|
if (language === "java" && node.type === "import_declaration") {
|
|
476
535
|
const spec = namedChildren(node).find((n) => n.type.endsWith("identifier"))?.text;
|
|
477
536
|
if (spec && !spec.endsWith(".*"))
|
|
@@ -1430,6 +1489,360 @@ function extractJavaProofCalls(root, imports) {
|
|
|
1430
1489
|
}
|
|
1431
1490
|
return out;
|
|
1432
1491
|
}
|
|
1492
|
+
const FASTAPI_HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
1493
|
+
function pythonStaticString(node) {
|
|
1494
|
+
if (!node || node.type !== "string")
|
|
1495
|
+
return null;
|
|
1496
|
+
const contents = node.descendantsOfType("string_content").filter((n) => Boolean(n));
|
|
1497
|
+
if (contents.length !== 1)
|
|
1498
|
+
return null;
|
|
1499
|
+
return contents[0]?.text ?? null;
|
|
1500
|
+
}
|
|
1501
|
+
function pythonDottedName(node) {
|
|
1502
|
+
if (!node)
|
|
1503
|
+
return null;
|
|
1504
|
+
if (node.type === "identifier" || node.type === "dotted_name")
|
|
1505
|
+
return node.text;
|
|
1506
|
+
if (node.type !== "attribute")
|
|
1507
|
+
return null;
|
|
1508
|
+
const object = pythonDottedName(node.childForFieldName("object"));
|
|
1509
|
+
const attribute = node.childForFieldName("attribute")?.text;
|
|
1510
|
+
return object && attribute ? `${object}.${attribute}` : null;
|
|
1511
|
+
}
|
|
1512
|
+
function pythonCallArguments(call) {
|
|
1513
|
+
const args = call.childForFieldName("arguments");
|
|
1514
|
+
return args ? namedChildren(args).filter((n) => n.type !== "comment") : [];
|
|
1515
|
+
}
|
|
1516
|
+
function pythonKeywordArgument(call, name) {
|
|
1517
|
+
for (const arg of pythonCallArguments(call)) {
|
|
1518
|
+
if (arg.type !== "keyword_argument" || arg.childForFieldName("name")?.text !== name)
|
|
1519
|
+
continue;
|
|
1520
|
+
return arg.childForFieldName("value") ?? namedChildren(arg)[1] ?? null;
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
function pythonPositionalArguments(call) {
|
|
1525
|
+
return pythonCallArguments(call).filter((n) => n.type !== "keyword_argument" && n.type !== "list_splat" && n.type !== "dictionary_splat");
|
|
1526
|
+
}
|
|
1527
|
+
function pythonStaticHttpMethods(node, fallback = ["GET"]) {
|
|
1528
|
+
if (!node)
|
|
1529
|
+
return fallback;
|
|
1530
|
+
if (!new Set(["list", "tuple", "set"]).has(node.type))
|
|
1531
|
+
return null;
|
|
1532
|
+
const elements = namedChildren(node).filter((child) => child.type !== "comment");
|
|
1533
|
+
if (elements.length === 0)
|
|
1534
|
+
return null;
|
|
1535
|
+
const methods = elements.map((element) => pythonStaticString(element));
|
|
1536
|
+
if (methods.some((method) => method === null))
|
|
1537
|
+
return null;
|
|
1538
|
+
const normalized = methods.map((method) => method.toLowerCase());
|
|
1539
|
+
return normalized.every((method) => FASTAPI_HTTP_METHODS.has(method)) ? normalized.map((method) => method.toUpperCase()) : null;
|
|
1540
|
+
}
|
|
1541
|
+
function normalizePythonRoutePath(...parts) {
|
|
1542
|
+
const joined = parts.map((p) => p.trim().replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
1543
|
+
return joined ? `/${joined}` : "/";
|
|
1544
|
+
}
|
|
1545
|
+
function pythonImportedFromFastapi(imports, local, imported) {
|
|
1546
|
+
return imports.some((b) => b.local === local && b.module === "fastapi" && b.kind === "named" && b.imported === imported);
|
|
1547
|
+
}
|
|
1548
|
+
function pythonCallIsFastapiConstructor(call, imports, imported) {
|
|
1549
|
+
const parts = callParts(call, "python");
|
|
1550
|
+
if (!parts)
|
|
1551
|
+
return false;
|
|
1552
|
+
if (!parts.qualifier)
|
|
1553
|
+
return pythonImportedFromFastapi(imports, parts.callee, imported);
|
|
1554
|
+
if (parts.callee !== imported)
|
|
1555
|
+
return false;
|
|
1556
|
+
const root = parts.qualifier.split(".")[0] ?? "";
|
|
1557
|
+
return imports.some((b) => b.local === root && b.kind === "module" && b.module === "fastapi");
|
|
1558
|
+
}
|
|
1559
|
+
function pythonBindingTargetNames(target) {
|
|
1560
|
+
if (!target)
|
|
1561
|
+
return [];
|
|
1562
|
+
if (target.type === "identifier")
|
|
1563
|
+
return [target.text];
|
|
1564
|
+
if (!new Set(["pattern_list", "tuple_pattern", "list_pattern", "tuple", "list"]).has(target.type))
|
|
1565
|
+
return [];
|
|
1566
|
+
return namedChildren(target).flatMap((child) => pythonBindingTargetNames(child));
|
|
1567
|
+
}
|
|
1568
|
+
function pythonFastapiReceivers(root, imports) {
|
|
1569
|
+
const mutationCount = new Map();
|
|
1570
|
+
const candidates = new Map();
|
|
1571
|
+
const mutations = [];
|
|
1572
|
+
for (const type of ["assignment", "annotated_assignment", "augmented_assignment", "delete_statement"]) {
|
|
1573
|
+
mutations.push(...root.descendantsOfType(type).filter((node) => Boolean(node)));
|
|
1574
|
+
}
|
|
1575
|
+
mutations.sort((a, b) => a.startIndex - b.startIndex);
|
|
1576
|
+
for (const assignment of mutations) {
|
|
1577
|
+
if (!pythonDirectModuleExecution(assignment))
|
|
1578
|
+
continue;
|
|
1579
|
+
const left = assignment.childForFieldName("left");
|
|
1580
|
+
const targets = assignment.type === "delete_statement"
|
|
1581
|
+
? namedChildren(assignment).flatMap((child) => pythonBindingTargetNames(child))
|
|
1582
|
+
: pythonBindingTargetNames(left);
|
|
1583
|
+
for (const target of targets)
|
|
1584
|
+
mutationCount.set(target, (mutationCount.get(target) ?? 0) + 1);
|
|
1585
|
+
const right = assignment.childForFieldName("right");
|
|
1586
|
+
if (left?.type !== "identifier")
|
|
1587
|
+
continue;
|
|
1588
|
+
const kind = right?.type === "call" && pythonCallIsFastapiConstructor(right, imports, "APIRouter")
|
|
1589
|
+
? "router"
|
|
1590
|
+
: right?.type === "call" && pythonCallIsFastapiConstructor(right, imports, "FastAPI")
|
|
1591
|
+
? "app"
|
|
1592
|
+
: null;
|
|
1593
|
+
if (!kind || right?.type !== "call")
|
|
1594
|
+
continue;
|
|
1595
|
+
const prefixNode = kind === "router" ? pythonKeywordArgument(right, "prefix") : null;
|
|
1596
|
+
const staticPrefix = pythonStaticString(prefixNode);
|
|
1597
|
+
candidates.set(left.text, {
|
|
1598
|
+
kind,
|
|
1599
|
+
prefix: staticPrefix ?? "",
|
|
1600
|
+
prefixResolved: !prefixNode || staticPrefix !== null,
|
|
1601
|
+
boundAt: assignment.startIndex
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
const out = new Map();
|
|
1605
|
+
for (const [name, receiver] of candidates)
|
|
1606
|
+
if (mutationCount.get(name) === 1)
|
|
1607
|
+
out.set(name, receiver);
|
|
1608
|
+
return out;
|
|
1609
|
+
}
|
|
1610
|
+
const PYTHON_NONDETERMINISTIC_SCOPES = new Set([
|
|
1611
|
+
"function_definition", "class_definition", "if_statement", "for_statement", "while_statement",
|
|
1612
|
+
"try_statement", "with_statement", "match_statement", "lambda", "list_comprehension",
|
|
1613
|
+
"dictionary_comprehension", "set_comprehension", "generator_expression"
|
|
1614
|
+
]);
|
|
1615
|
+
function pythonDirectModuleExecution(node) {
|
|
1616
|
+
let parent = node.parent;
|
|
1617
|
+
while (parent) {
|
|
1618
|
+
if (PYTHON_NONDETERMINISTIC_SCOPES.has(parent.type))
|
|
1619
|
+
return false;
|
|
1620
|
+
if (parent.type === "module")
|
|
1621
|
+
return true;
|
|
1622
|
+
parent = parent.parent;
|
|
1623
|
+
}
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
function pythonRouterIncludePrefixes(root, receivers) {
|
|
1627
|
+
const out = new Map();
|
|
1628
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1629
|
+
if (!call || !pythonDirectModuleExecution(call))
|
|
1630
|
+
continue;
|
|
1631
|
+
const parts = callParts(call, "python");
|
|
1632
|
+
const app = parts?.qualifier?.split(".")[0];
|
|
1633
|
+
if (!parts || parts.callee !== "include_router" || !app || receivers.get(app)?.kind !== "app")
|
|
1634
|
+
continue;
|
|
1635
|
+
const router = pythonDottedName(pythonPositionalArguments(call)[0] ?? null);
|
|
1636
|
+
if (!router || !receivers.has(router) || receivers.get(app).boundAt >= call.startIndex || receivers.get(router).boundAt >= call.startIndex)
|
|
1637
|
+
continue;
|
|
1638
|
+
const includePrefix = pythonStaticString(pythonKeywordArgument(call, "prefix"));
|
|
1639
|
+
if (includePrefix === null && pythonKeywordArgument(call, "prefix"))
|
|
1640
|
+
continue;
|
|
1641
|
+
let prefixes = out.get(router);
|
|
1642
|
+
if (!prefixes)
|
|
1643
|
+
out.set(router, (prefixes = new Set()));
|
|
1644
|
+
prefixes.add(includePrefix ?? "");
|
|
1645
|
+
}
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1648
|
+
function makePythonBehaviorContract(input) {
|
|
1649
|
+
const identityInput = `${input.method}|${input.path}|${input.file}|${input.handler ?? ""}`;
|
|
1650
|
+
return {
|
|
1651
|
+
id: `endpoint:${slugify(`${input.method}-${input.path}-${input.file}-${input.handler ?? ""}`)}-${shortHash(identityInput)}`,
|
|
1652
|
+
title: `${input.method} ${input.path}`,
|
|
1653
|
+
kind: "http_endpoint",
|
|
1654
|
+
framework: "fastapi",
|
|
1655
|
+
method: input.method,
|
|
1656
|
+
path: input.path,
|
|
1657
|
+
file: input.file,
|
|
1658
|
+
handler: input.handler,
|
|
1659
|
+
route_prefix_status: input.prefixStatus,
|
|
1660
|
+
...(input.prefix ? { route_prefix: input.prefix } : {}),
|
|
1661
|
+
source: "framework"
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function dedupePythonContracts(contracts) {
|
|
1665
|
+
const seen = new Set();
|
|
1666
|
+
return contracts.filter((contract) => {
|
|
1667
|
+
const key = `${contract.method}|${contract.path}|${contract.handler ?? ""}`;
|
|
1668
|
+
if (seen.has(key))
|
|
1669
|
+
return false;
|
|
1670
|
+
seen.add(key);
|
|
1671
|
+
return true;
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
function pythonDirectModuleHandlers(root) {
|
|
1675
|
+
const counts = new Map();
|
|
1676
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
1677
|
+
if (!fn || hasPythonFunctionAncestor(fn))
|
|
1678
|
+
continue;
|
|
1679
|
+
const declaration = fn.parent?.type === "decorated_definition" ? fn.parent : fn;
|
|
1680
|
+
if (declaration.parent?.type !== "module")
|
|
1681
|
+
continue;
|
|
1682
|
+
const name = pythonQualifiedFunctionName(fn);
|
|
1683
|
+
if (name)
|
|
1684
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1685
|
+
}
|
|
1686
|
+
for (const type of ["assignment", "annotated_assignment", "augmented_assignment", "delete_statement"]) {
|
|
1687
|
+
for (const mutation of root.descendantsOfType(type)) {
|
|
1688
|
+
if (!mutation || !pythonDirectModuleExecution(mutation))
|
|
1689
|
+
continue;
|
|
1690
|
+
const left = mutation.childForFieldName("left");
|
|
1691
|
+
const targets = mutation.type === "delete_statement"
|
|
1692
|
+
? namedChildren(mutation).flatMap((child) => pythonBindingTargetNames(child))
|
|
1693
|
+
: pythonBindingTargetNames(left);
|
|
1694
|
+
for (const target of targets)
|
|
1695
|
+
if (counts.has(target))
|
|
1696
|
+
counts.set(target, counts.get(target) + 1);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return new Set([...counts].filter(([, count]) => count === 1).map(([name]) => name));
|
|
1700
|
+
}
|
|
1701
|
+
function extractPythonBehaviorContracts(root, imports, file) {
|
|
1702
|
+
const receivers = pythonFastapiReceivers(root, imports);
|
|
1703
|
+
if (receivers.size === 0)
|
|
1704
|
+
return [];
|
|
1705
|
+
const directHandlers = pythonDirectModuleHandlers(root);
|
|
1706
|
+
const includes = pythonRouterIncludePrefixes(root, receivers);
|
|
1707
|
+
const out = [];
|
|
1708
|
+
const add = (receiverName, method, routePath, registrationAt, handler) => {
|
|
1709
|
+
const receiver = receivers.get(receiverName);
|
|
1710
|
+
if (!receiver || receiver.boundAt >= registrationAt)
|
|
1711
|
+
return;
|
|
1712
|
+
const includePrefixes = receiver.kind === "app" ? new Set([""]) : includes.get(receiverName);
|
|
1713
|
+
const prefixes = includePrefixes && includePrefixes.size > 0 ? [...includePrefixes] : [""];
|
|
1714
|
+
const status = receiver.prefixResolved && (receiver.kind === "app" || Boolean(includePrefixes?.size)) ? "resolved" : "unresolved";
|
|
1715
|
+
for (const includePrefix of prefixes) {
|
|
1716
|
+
const prefix = normalizePythonRoutePath(includePrefix, receiver.prefix);
|
|
1717
|
+
const normalizedPrefix = prefix === "/" ? "" : prefix;
|
|
1718
|
+
out.push(makePythonBehaviorContract({
|
|
1719
|
+
file,
|
|
1720
|
+
method: method.toUpperCase(),
|
|
1721
|
+
path: normalizePythonRoutePath(normalizedPrefix, routePath),
|
|
1722
|
+
handler,
|
|
1723
|
+
prefixStatus: status,
|
|
1724
|
+
prefix: normalizedPrefix
|
|
1725
|
+
}));
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
1729
|
+
if (!fn || hasPythonFunctionAncestor(fn))
|
|
1730
|
+
continue;
|
|
1731
|
+
const handler = pythonQualifiedFunctionName(fn);
|
|
1732
|
+
const decorated = fn.parent?.type === "decorated_definition" ? fn.parent : null;
|
|
1733
|
+
if (!handler || !directHandlers.has(handler) || !decorated || decorated.parent?.type !== "module")
|
|
1734
|
+
continue;
|
|
1735
|
+
for (const decorator of namedChildren(decorated).filter((n) => n.type === "decorator")) {
|
|
1736
|
+
const call = namedChildren(decorator).find((n) => n.type === "call");
|
|
1737
|
+
const parts = call ? callParts(call, "python") : null;
|
|
1738
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1739
|
+
const route = call ? pythonStaticString(pythonPositionalArguments(call)[0] ?? null) : null;
|
|
1740
|
+
if (!call || !parts || !receiver || route === null)
|
|
1741
|
+
continue;
|
|
1742
|
+
if (FASTAPI_HTTP_METHODS.has(parts.callee)) {
|
|
1743
|
+
add(receiver, parts.callee, route, call.startIndex, handler);
|
|
1744
|
+
}
|
|
1745
|
+
else if (parts.callee === "api_route") {
|
|
1746
|
+
const methods = pythonStaticHttpMethods(pythonKeywordArgument(call, "methods"));
|
|
1747
|
+
if (!methods)
|
|
1748
|
+
continue;
|
|
1749
|
+
for (const method of methods)
|
|
1750
|
+
add(receiver, method, route, call.startIndex, handler);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1755
|
+
if (!call || !pythonDirectModuleExecution(call))
|
|
1756
|
+
continue;
|
|
1757
|
+
const parts = callParts(call, "python");
|
|
1758
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1759
|
+
if (!parts || parts.callee !== "add_api_route" || !receiver || !receivers.has(receiver))
|
|
1760
|
+
continue;
|
|
1761
|
+
const positional = pythonPositionalArguments(call);
|
|
1762
|
+
const route = pythonStaticString(positional[0] ?? null);
|
|
1763
|
+
const handler = pythonDottedName(positional[1] ?? null);
|
|
1764
|
+
if (route === null || !handler || !directHandlers.has(handler))
|
|
1765
|
+
continue;
|
|
1766
|
+
const methods = pythonStaticHttpMethods(pythonKeywordArgument(call, "methods"));
|
|
1767
|
+
if (!methods)
|
|
1768
|
+
continue;
|
|
1769
|
+
for (const method of methods)
|
|
1770
|
+
add(receiver, method, route, call.startIndex, handler);
|
|
1771
|
+
}
|
|
1772
|
+
return dedupePythonContracts(out);
|
|
1773
|
+
}
|
|
1774
|
+
function isFastapiDependsCall(call, imports) {
|
|
1775
|
+
const parts = callParts(call, "python");
|
|
1776
|
+
if (!parts)
|
|
1777
|
+
return false;
|
|
1778
|
+
if (!parts.qualifier) {
|
|
1779
|
+
return pythonImportedFromFastapi(imports, parts.callee, "Depends") || pythonImportedFromFastapi(imports, parts.callee, "Security");
|
|
1780
|
+
}
|
|
1781
|
+
if (parts.callee !== "Depends" && parts.callee !== "Security")
|
|
1782
|
+
return false;
|
|
1783
|
+
const root = parts.qualifier.split(".")[0] ?? "";
|
|
1784
|
+
return imports.some((b) => b.local === root && b.kind === "module" && b.module === "fastapi");
|
|
1785
|
+
}
|
|
1786
|
+
function extractPythonDependencies(root, imports, acceptedHandlers) {
|
|
1787
|
+
const out = [];
|
|
1788
|
+
const seen = new Set();
|
|
1789
|
+
const receivers = pythonFastapiReceivers(root, imports);
|
|
1790
|
+
const directHandlers = pythonDirectModuleHandlers(root);
|
|
1791
|
+
const addFromNode = (handler, node) => {
|
|
1792
|
+
if (!node)
|
|
1793
|
+
return;
|
|
1794
|
+
const calls = [...(node.type === "call" ? [node] : []), ...node.descendantsOfType("call").filter((n) => Boolean(n))];
|
|
1795
|
+
for (const call of calls) {
|
|
1796
|
+
if (!isFastapiDependsCall(call, imports))
|
|
1797
|
+
continue;
|
|
1798
|
+
const dependency = pythonDottedName(pythonPositionalArguments(call)[0] ?? null);
|
|
1799
|
+
if (!dependency)
|
|
1800
|
+
continue;
|
|
1801
|
+
const key = `${handler}|${dependency}`;
|
|
1802
|
+
if (seen.has(key))
|
|
1803
|
+
continue;
|
|
1804
|
+
seen.add(key);
|
|
1805
|
+
out.push({ handler, dependency });
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
1809
|
+
if (!fn || hasPythonFunctionAncestor(fn))
|
|
1810
|
+
continue;
|
|
1811
|
+
const handler = pythonQualifiedFunctionName(fn);
|
|
1812
|
+
const params = fn.childForFieldName("parameters");
|
|
1813
|
+
const declaration = fn.parent?.type === "decorated_definition" ? fn.parent : fn;
|
|
1814
|
+
if (!handler || !params || !directHandlers.has(handler) || declaration.parent?.type !== "module" || !acceptedHandlers.has(handler))
|
|
1815
|
+
continue;
|
|
1816
|
+
addFromNode(handler, params);
|
|
1817
|
+
const decorated = fn.parent?.type === "decorated_definition" ? fn.parent : null;
|
|
1818
|
+
for (const decorator of decorated ? namedChildren(decorated).filter((n) => n.type === "decorator") : []) {
|
|
1819
|
+
const routeCall = namedChildren(decorator).find((n) => n.type === "call");
|
|
1820
|
+
const parts = routeCall ? callParts(routeCall, "python") : null;
|
|
1821
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1822
|
+
if (!routeCall || !parts || !receiver || !receivers.has(receiver) || receivers.get(receiver).boundAt >= routeCall.startIndex)
|
|
1823
|
+
continue;
|
|
1824
|
+
if (!FASTAPI_HTTP_METHODS.has(parts.callee) && (parts.callee !== "api_route" || !pythonStaticHttpMethods(pythonKeywordArgument(routeCall, "methods"))))
|
|
1825
|
+
continue;
|
|
1826
|
+
addFromNode(handler, pythonKeywordArgument(routeCall, "dependencies"));
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1830
|
+
if (!call || !pythonDirectModuleExecution(call))
|
|
1831
|
+
continue;
|
|
1832
|
+
const parts = callParts(call, "python");
|
|
1833
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1834
|
+
if (!parts || parts.callee !== "add_api_route" || !receiver || !receivers.has(receiver) || receivers.get(receiver).boundAt >= call.startIndex)
|
|
1835
|
+
continue;
|
|
1836
|
+
const positional = pythonPositionalArguments(call);
|
|
1837
|
+
const route = pythonStaticString(positional[0] ?? null);
|
|
1838
|
+
const handler = pythonDottedName(positional[1] ?? null);
|
|
1839
|
+
const methods = pythonStaticHttpMethods(pythonKeywordArgument(call, "methods"));
|
|
1840
|
+
if (route !== null && handler && methods && directHandlers.has(handler) && acceptedHandlers.has(handler)) {
|
|
1841
|
+
addFromNode(handler, pythonKeywordArgument(call, "dependencies"));
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return out;
|
|
1845
|
+
}
|
|
1433
1846
|
function directPythonAssertCall(assertion) {
|
|
1434
1847
|
const subject = namedChildren(assertion)[0];
|
|
1435
1848
|
if (!subject)
|
|
@@ -1443,19 +1856,195 @@ function singlePythonComparisonCall(comparison) {
|
|
|
1443
1856
|
const calls = namedChildren(comparison).filter((child) => child.type === "call");
|
|
1444
1857
|
return calls.length === 1 ? calls[0] ?? null : null;
|
|
1445
1858
|
}
|
|
1859
|
+
function pythonDirectAssignment(stmt) {
|
|
1860
|
+
const assignment = stmt.type === "assignment" || stmt.type === "annotated_assignment"
|
|
1861
|
+
? stmt
|
|
1862
|
+
: namedChildren(stmt).find((child) => child.type === "assignment" || child.type === "annotated_assignment");
|
|
1863
|
+
if (!assignment)
|
|
1864
|
+
return null;
|
|
1865
|
+
const target = assignment.childForFieldName("left");
|
|
1866
|
+
const value = assignment.childForFieldName("right");
|
|
1867
|
+
return target && value ? { target, value } : null;
|
|
1868
|
+
}
|
|
1869
|
+
function pythonSingleCall(node) {
|
|
1870
|
+
const calls = [...(node.type === "call" ? [node] : []), ...node.descendantsOfType("call").filter((call) => Boolean(call))];
|
|
1871
|
+
const unique = [...new Map(calls.map((call) => [`${call.startIndex}:${call.endIndex}`, call])).values()];
|
|
1872
|
+
return unique.length === 1 ? unique[0] ?? null : null;
|
|
1873
|
+
}
|
|
1874
|
+
function pythonExactCall(node) {
|
|
1875
|
+
if (!node)
|
|
1876
|
+
return null;
|
|
1877
|
+
if (node.type === "call")
|
|
1878
|
+
return node;
|
|
1879
|
+
if (node.type === "await" || node.type === "parenthesized_expression") {
|
|
1880
|
+
const children = namedChildren(node);
|
|
1881
|
+
return children.length === 1 ? pythonExactCall(children[0] ?? null) : null;
|
|
1882
|
+
}
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
function pythonDirectAssertedResult(assertion, results) {
|
|
1886
|
+
const subject = namedChildren(assertion)[0];
|
|
1887
|
+
if (subject?.type !== "comparison_operator")
|
|
1888
|
+
return null;
|
|
1889
|
+
const operands = namedChildren(subject);
|
|
1890
|
+
if (operands.length !== 2)
|
|
1891
|
+
return null;
|
|
1892
|
+
const candidates = operands.filter((operand) => operand.type === "identifier" && results.has(operand.text));
|
|
1893
|
+
if (candidates.length !== 1)
|
|
1894
|
+
return null;
|
|
1895
|
+
const name = candidates[0].text;
|
|
1896
|
+
const occurrences = subject.descendantsOfType("identifier")
|
|
1897
|
+
.filter((identifier) => Boolean(identifier))
|
|
1898
|
+
.filter((identifier) => identifier.text === name);
|
|
1899
|
+
return occurrences.length === 1 ? name : null;
|
|
1900
|
+
}
|
|
1901
|
+
function pythonLexicalNodes(root) {
|
|
1902
|
+
const out = [];
|
|
1903
|
+
const visit = (node) => {
|
|
1904
|
+
for (const child of namedChildren(node)) {
|
|
1905
|
+
if (child.type === "function_definition" || child.type === "class_definition" || child.type === "lambda")
|
|
1906
|
+
continue;
|
|
1907
|
+
out.push(child);
|
|
1908
|
+
visit(child);
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
visit(root);
|
|
1912
|
+
return out;
|
|
1913
|
+
}
|
|
1914
|
+
function pythonBindingNames(node) {
|
|
1915
|
+
if (node.type === "assignment" || node.type === "annotated_assignment" || node.type === "augmented_assignment" || node.type === "named_expression") {
|
|
1916
|
+
const target = node.childForFieldName("left") ?? node.childForFieldName("name");
|
|
1917
|
+
if (target?.type === "attribute") {
|
|
1918
|
+
const dotted = pythonDottedName(target);
|
|
1919
|
+
return dotted ? [dotted.split(".")[0] ?? dotted] : [];
|
|
1920
|
+
}
|
|
1921
|
+
return target ? pythonBindingTargetNames(target) : [];
|
|
1922
|
+
}
|
|
1923
|
+
if (node.type === "for_statement") {
|
|
1924
|
+
const target = node.childForFieldName("left");
|
|
1925
|
+
return target ? pythonBindingTargetNames(target) : [];
|
|
1926
|
+
}
|
|
1927
|
+
if (node.type === "delete_statement") {
|
|
1928
|
+
return namedChildren(node).flatMap((target) => {
|
|
1929
|
+
if (target.type === "attribute") {
|
|
1930
|
+
const dotted = pythonDottedName(target);
|
|
1931
|
+
return dotted ? [dotted.split(".")[0] ?? dotted] : [];
|
|
1932
|
+
}
|
|
1933
|
+
return pythonBindingTargetNames(target);
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
return [];
|
|
1937
|
+
}
|
|
1938
|
+
function pythonAssignedInstanceProofCalls(block) {
|
|
1939
|
+
const testFunction = block.parent;
|
|
1940
|
+
const decorated = testFunction?.parent?.type === "decorated_definition" ? testFunction.parent : testFunction;
|
|
1941
|
+
const parameterNames = testFunction?.childForFieldName("parameters")?.descendantsOfType("identifier").map((node) => node?.text).filter(Boolean) ?? [];
|
|
1942
|
+
if (parameterNames.some((name) => name !== "self" && name !== "cls"))
|
|
1943
|
+
return [];
|
|
1944
|
+
if (/\b(?:monkeypatch|mocker|patch|Mock|MagicMock|AsyncMock|create_autospec)\b/.test(decorated?.text ?? ""))
|
|
1945
|
+
return [];
|
|
1946
|
+
const lexicalNodes = pythonLexicalNodes(block);
|
|
1947
|
+
const assignments = lexicalNodes.filter((node) => node.type === "assignment" || node.type === "annotated_assignment");
|
|
1948
|
+
const assertions = lexicalNodes.filter((node) => node.type === "assert_statement");
|
|
1949
|
+
if (assertions.length !== 1)
|
|
1950
|
+
return [];
|
|
1951
|
+
const mutationCounts = new Map();
|
|
1952
|
+
for (const node of lexicalNodes) {
|
|
1953
|
+
for (const name of pythonBindingNames(node))
|
|
1954
|
+
mutationCounts.set(name, (mutationCounts.get(name) ?? 0) + 1);
|
|
1955
|
+
}
|
|
1956
|
+
const instances = new Map();
|
|
1957
|
+
const orderedAssignments = assignments.map(pythonDirectAssignment).filter((assignment) => Boolean(assignment));
|
|
1958
|
+
for (const { target, value } of orderedAssignments) {
|
|
1959
|
+
if (target.type !== "identifier" || mutationCounts.get(target.text) !== 1)
|
|
1960
|
+
continue;
|
|
1961
|
+
const constructor = pythonSingleCall(value);
|
|
1962
|
+
const parts = constructor ? callParts(constructor, "python") : null;
|
|
1963
|
+
if (!parts || parts.qualifier || !/^[A-Za-z_]\w*$/.test(parts.callee))
|
|
1964
|
+
continue;
|
|
1965
|
+
if ((mutationCounts.get(parts.callee) ?? 0) > 0)
|
|
1966
|
+
continue;
|
|
1967
|
+
instances.set(target.text, { className: parts.callee, assignedAt: target.startIndex });
|
|
1968
|
+
}
|
|
1969
|
+
for (const { target, value } of orderedAssignments) {
|
|
1970
|
+
if (target.type === "identifier" && value.type === "identifier" && instances.has(value.text))
|
|
1971
|
+
return [];
|
|
1972
|
+
}
|
|
1973
|
+
const trackedNames = new Set([
|
|
1974
|
+
...instances.keys(),
|
|
1975
|
+
...[...instances.values()].map((instance) => instance.className)
|
|
1976
|
+
]);
|
|
1977
|
+
for (const callNode of lexicalNodes.filter((node) => node.type === "call")) {
|
|
1978
|
+
const call = callParts(callNode, "python");
|
|
1979
|
+
const receiver = call?.qualifier?.split(".")[0] ?? "";
|
|
1980
|
+
if (instances.has(receiver))
|
|
1981
|
+
continue;
|
|
1982
|
+
const args = callNode.childForFieldName("arguments");
|
|
1983
|
+
const argumentNames = args
|
|
1984
|
+
? [
|
|
1985
|
+
...(args.type === "identifier" ? [args] : []),
|
|
1986
|
+
...args.descendantsOfType("identifier").filter((identifier) => Boolean(identifier))
|
|
1987
|
+
].map((identifier) => identifier.text)
|
|
1988
|
+
: [];
|
|
1989
|
+
if (argumentNames.some((name) => trackedNames.has(name)))
|
|
1990
|
+
return [];
|
|
1991
|
+
}
|
|
1992
|
+
const results = new Map();
|
|
1993
|
+
const instanceCalls = lexicalNodes.filter((node) => node.type === "call").filter((callNode) => {
|
|
1994
|
+
const call = callParts(callNode, "python");
|
|
1995
|
+
const receiver = call?.qualifier?.split(".")[0] ?? "";
|
|
1996
|
+
return instances.has(receiver);
|
|
1997
|
+
});
|
|
1998
|
+
if (instanceCalls.length !== 1)
|
|
1999
|
+
return [];
|
|
2000
|
+
for (const { target, value } of orderedAssignments) {
|
|
2001
|
+
if (target.type !== "identifier" || mutationCounts.get(target.text) !== 1)
|
|
2002
|
+
continue;
|
|
2003
|
+
const callNode = pythonExactCall(value);
|
|
2004
|
+
const call = callNode ? callParts(callNode, "python") : null;
|
|
2005
|
+
const receiver = call?.qualifier?.split(".")[0] ?? "";
|
|
2006
|
+
const instance = instances.get(receiver);
|
|
2007
|
+
if (call && instance && instance.assignedAt < value.startIndex) {
|
|
2008
|
+
results.set(target.text, { call, instanceClass: instance.className });
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
const out = [];
|
|
2012
|
+
for (const stmt of assertions) {
|
|
2013
|
+
const direct = directPythonAssertCall(stmt);
|
|
2014
|
+
const directReceiver = direct?.qualifier?.split(".")[0] ?? "";
|
|
2015
|
+
const directInstance = instances.get(directReceiver);
|
|
2016
|
+
if (direct && directInstance && directInstance.assignedAt < stmt.startIndex) {
|
|
2017
|
+
out.push({ assertion: stmt, call: direct, instanceClass: directInstance.className });
|
|
2018
|
+
}
|
|
2019
|
+
const subject = namedChildren(stmt)[0];
|
|
2020
|
+
const assertionCalls = subject
|
|
2021
|
+
? [...(subject.type === "call" ? [subject] : []), ...subject.descendantsOfType("call").filter((call) => Boolean(call))]
|
|
2022
|
+
: [];
|
|
2023
|
+
if (assertionCalls.length > 0)
|
|
2024
|
+
continue;
|
|
2025
|
+
const observed = pythonDirectAssertedResult(stmt, results);
|
|
2026
|
+
const result = observed ? results.get(observed) : undefined;
|
|
2027
|
+
if (result)
|
|
2028
|
+
out.push({ assertion: stmt, ...result });
|
|
2029
|
+
}
|
|
2030
|
+
return out;
|
|
2031
|
+
}
|
|
1446
2032
|
function extractPythonProofCalls(root) {
|
|
1447
2033
|
const out = [];
|
|
1448
2034
|
const seen = new Set();
|
|
1449
|
-
const add = (testName, shadowed, call) => {
|
|
2035
|
+
const add = (testName, shadowed, call, instanceClass) => {
|
|
1450
2036
|
if (!call)
|
|
1451
2037
|
return;
|
|
1452
|
-
const key = `${testName}|${call.qualifier ?? ""}|${call.callee}`;
|
|
2038
|
+
const key = `${testName}|${instanceClass ?? ""}|${call.qualifier ?? ""}|${call.callee}`;
|
|
1453
2039
|
if (seen.has(key))
|
|
1454
2040
|
return;
|
|
1455
2041
|
seen.add(key);
|
|
1456
|
-
out.push({ caller: testName, testName, ...call, shadowed: [...shadowed], assertion: "pytest_assert" });
|
|
2042
|
+
out.push({ caller: testName, testName, ...call, shadowed: [...shadowed], assertion: "pytest_assert", ...(instanceClass ? { instanceClass } : {}) });
|
|
1457
2043
|
};
|
|
1458
2044
|
const processBlock = (block, testName, shadowed) => {
|
|
2045
|
+
for (const associated of pythonAssignedInstanceProofCalls(block)) {
|
|
2046
|
+
add(testName, shadowed, associated.call, associated.instanceClass);
|
|
2047
|
+
}
|
|
1459
2048
|
for (const stmt of blockStatements(block)) {
|
|
1460
2049
|
if (stmt.type === "assert_statement")
|
|
1461
2050
|
add(testName, shadowed, directPythonAssertCall(stmt));
|
|
@@ -1490,7 +2079,7 @@ function extractPythonProofCalls(root) {
|
|
|
1490
2079
|
walk(root, false, null);
|
|
1491
2080
|
return out;
|
|
1492
2081
|
}
|
|
1493
|
-
export function extractTreeSitterStructure(content, language) {
|
|
2082
|
+
export function extractTreeSitterStructure(content, language, relPath = "") {
|
|
1494
2083
|
const parser = parserFor(language);
|
|
1495
2084
|
if (!parser)
|
|
1496
2085
|
return { imports: [], calls: [] };
|
|
@@ -1530,6 +2119,9 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1530
2119
|
callerReceivers.set(name, { receiverVar: rv, receiverType: recv });
|
|
1531
2120
|
}
|
|
1532
2121
|
}
|
|
2122
|
+
if (name && language === "python" && node.type === "function_definition") {
|
|
2123
|
+
name = pythonQualifiedFunctionName(node) ?? name;
|
|
2124
|
+
}
|
|
1533
2125
|
if (name) {
|
|
1534
2126
|
nextCaller = name;
|
|
1535
2127
|
nextShadowed = localBindings(node, language);
|
|
@@ -1546,6 +2138,10 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1546
2138
|
};
|
|
1547
2139
|
visit(root, null, new Set(), false);
|
|
1548
2140
|
const imports = extractImports(root, language);
|
|
2141
|
+
const pythonBehaviorContracts = language === "python" ? extractPythonBehaviorContracts(root, imports, relPath) : undefined;
|
|
2142
|
+
const pythonDependencies = language === "python"
|
|
2143
|
+
? extractPythonDependencies(root, imports, new Set((pythonBehaviorContracts ?? []).map((contract) => contract.handler).filter((handler) => Boolean(handler))))
|
|
2144
|
+
: undefined;
|
|
1549
2145
|
const result = {
|
|
1550
2146
|
...(language === "java" ? { packageName: javaPackage(root), javaClasses: javaClassInfos(root), javaProofCalls: extractJavaProofCalls(root, imports) } : {}),
|
|
1551
2147
|
...(language === "go" ? { packageName: goPackage(root), goCtorResults: extractGoCtorResults(root) } : {}),
|
|
@@ -1555,7 +2151,7 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1555
2151
|
imports,
|
|
1556
2152
|
calls,
|
|
1557
2153
|
...(language === "go" ? { goProofCalls: extractGoProofCalls(root, imports) } : {}),
|
|
1558
|
-
...(language === "python" ? { pythonProofCalls: extractPythonProofCalls(root) } : {})
|
|
2154
|
+
...(language === "python" ? { pythonProofCalls: extractPythonProofCalls(root), pythonBehaviorContracts, pythonDependencies } : {})
|
|
1559
2155
|
};
|
|
1560
2156
|
tree.delete();
|
|
1561
2157
|
return result;
|