@orangepro/orangepro-mcp 0.2.43 → 0.2.45

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.
@@ -342,8 +342,20 @@ function localBindings(fn, language) {
342
342
  const out = new Set();
343
343
  const params = fn.childForFieldName("parameters") ?? (language === "kotlin" ? namedChildren(fn).find((n) => n.type === "function_value_parameters") : null);
344
344
  if (params) {
345
- if (language === "python")
346
- collectNames(params, out);
345
+ if (language === "python") {
346
+ // Parameter annotations are references, not local bindings. Collecting every
347
+ // identifier from `value: ImportedType` incorrectly shadowed the imported
348
+ // production class and made an otherwise exact structural association fail.
349
+ for (const parameter of namedChildren(params)) {
350
+ if (parameter.type === "identifier")
351
+ out.add(parameter.text);
352
+ else {
353
+ const name = parameter.childForFieldName("name") ?? namedChildren(parameter)[0] ?? null;
354
+ if (name?.type === "identifier")
355
+ out.add(name.text);
356
+ }
357
+ }
358
+ }
347
359
  else {
348
360
  for (const child of namedChildren(params)) {
349
361
  if (/parameter/.test(child.type))
@@ -1856,34 +1868,744 @@ function singlePythonComparisonCall(comparison) {
1856
1868
  const calls = namedChildren(comparison).filter((child) => child.type === "call");
1857
1869
  return calls.length === 1 ? calls[0] ?? null : null;
1858
1870
  }
1859
- function extractPythonProofCalls(root) {
1871
+ function pythonDirectAssignment(stmt) {
1872
+ const assignment = stmt.type === "assignment" || stmt.type === "annotated_assignment"
1873
+ ? stmt
1874
+ : namedChildren(stmt).find((child) => child.type === "assignment" || child.type === "annotated_assignment");
1875
+ if (!assignment)
1876
+ return null;
1877
+ const target = assignment.childForFieldName("left");
1878
+ const value = assignment.childForFieldName("right");
1879
+ return target && value ? { target, value } : null;
1880
+ }
1881
+ function pythonSingleCall(node) {
1882
+ const calls = [...(node.type === "call" ? [node] : []), ...node.descendantsOfType("call").filter((call) => Boolean(call))];
1883
+ const unique = [...new Map(calls.map((call) => [`${call.startIndex}:${call.endIndex}`, call])).values()];
1884
+ return unique.length === 1 ? unique[0] ?? null : null;
1885
+ }
1886
+ function pythonExactCall(node) {
1887
+ if (!node)
1888
+ return null;
1889
+ if (node.type === "call")
1890
+ return node;
1891
+ if (node.type === "await" || node.type === "parenthesized_expression") {
1892
+ const children = namedChildren(node);
1893
+ return children.length === 1 ? pythonExactCall(children[0] ?? null) : null;
1894
+ }
1895
+ return null;
1896
+ }
1897
+ function pythonDirectAssertedResult(assertion, results) {
1898
+ const subject = namedChildren(assertion)[0];
1899
+ if (subject?.type !== "comparison_operator")
1900
+ return null;
1901
+ const operands = namedChildren(subject);
1902
+ if (operands.length !== 2)
1903
+ return null;
1904
+ const candidates = operands.filter((operand) => operand.type === "identifier" && results.has(operand.text));
1905
+ if (candidates.length !== 1)
1906
+ return null;
1907
+ const name = candidates[0].text;
1908
+ const occurrences = subject.descendantsOfType("identifier")
1909
+ .filter((identifier) => Boolean(identifier))
1910
+ .filter((identifier) => identifier.text === name);
1911
+ return occurrences.length === 1 ? name : null;
1912
+ }
1913
+ function pythonLexicalNodes(root) {
1914
+ const out = [];
1915
+ const visit = (node) => {
1916
+ for (const child of namedChildren(node)) {
1917
+ if (child.type === "function_definition" || child.type === "class_definition" || child.type === "lambda")
1918
+ continue;
1919
+ out.push(child);
1920
+ visit(child);
1921
+ }
1922
+ };
1923
+ visit(root);
1924
+ return out;
1925
+ }
1926
+ function pythonBindingNames(node) {
1927
+ if (node.type === "assignment" || node.type === "annotated_assignment" || node.type === "augmented_assignment" || node.type === "named_expression") {
1928
+ const target = node.childForFieldName("left") ?? node.childForFieldName("name");
1929
+ if (target?.type === "attribute") {
1930
+ const dotted = pythonDottedName(target);
1931
+ return dotted ? [dotted.split(".")[0] ?? dotted] : [];
1932
+ }
1933
+ return target ? pythonBindingTargetNames(target) : [];
1934
+ }
1935
+ if (node.type === "for_statement") {
1936
+ const target = node.childForFieldName("left");
1937
+ return target ? pythonBindingTargetNames(target) : [];
1938
+ }
1939
+ if (node.type === "delete_statement") {
1940
+ return namedChildren(node).flatMap((target) => {
1941
+ if (target.type === "attribute") {
1942
+ const dotted = pythonDottedName(target);
1943
+ return dotted ? [dotted.split(".")[0] ?? dotted] : [];
1944
+ }
1945
+ return pythonBindingTargetNames(target);
1946
+ });
1947
+ }
1948
+ return [];
1949
+ }
1950
+ function pythonClassRefFromConstructor(value) {
1951
+ if (!value)
1952
+ return null;
1953
+ const constructor = pythonExactCall(value);
1954
+ if (!constructor)
1955
+ return null;
1956
+ const ref = pythonDottedName(constructor.childForFieldName("function"));
1957
+ // A result-producing method such as `subject.work()` is not a constructor and
1958
+ // must never become a second tracked receiver. Accept only a bare imported
1959
+ // class or one module-qualified class terminal.
1960
+ return ref && /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?$/.test(ref) && pythonLooksLikeClassRef(ref) ? ref : null;
1961
+ }
1962
+ function pythonLooksLikeClassRef(ref) {
1963
+ const terminal = ref.split(".").pop() ?? "";
1964
+ return /^_*[A-Z][A-Za-z0-9_]*$/.test(terminal);
1965
+ }
1966
+ function pythonReceiverKey(node) {
1967
+ if (!node)
1968
+ return null;
1969
+ if (node.type === "identifier")
1970
+ return node.text;
1971
+ if (node.type !== "attribute")
1972
+ return null;
1973
+ const object = node.childForFieldName("object");
1974
+ const attribute = node.childForFieldName("attribute")?.text;
1975
+ return object?.type === "identifier" && object.text === "self" && attribute && /^[A-Za-z_]\w*$/.test(attribute)
1976
+ ? `self.${attribute}`
1977
+ : null;
1978
+ }
1979
+ function pythonMethodCall(node) {
1980
+ if (node.type !== "call")
1981
+ return null;
1982
+ const fn = node.childForFieldName("function");
1983
+ if (fn?.type !== "attribute")
1984
+ return null;
1985
+ const receiver = fn.childForFieldName("object");
1986
+ const call = callParts(node, "python");
1987
+ return receiver && call ? { call, receiver } : null;
1988
+ }
1989
+ function directPythonAssertCallNode(assertion) {
1990
+ const subject = namedChildren(assertion)[0];
1991
+ if (!subject)
1992
+ return null;
1993
+ const actual = subject.type === "comparison_operator" ? singlePythonComparisonCall(subject) : subject;
1994
+ return actual?.type === "call" ? actual : null;
1995
+ }
1996
+ function pythonFixtureDecorator(node) {
1997
+ return /^@pytest\.fixture(?:\(|$)/.test(node.text.trim());
1998
+ }
1999
+ function pythonTypeRef(node) {
2000
+ const text = node?.text ?? "";
2001
+ return /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?$/.test(text) ? text : null;
2002
+ }
2003
+ function pythonCanonicalImportedRef(ref, imports) {
2004
+ if (!ref)
2005
+ return undefined;
2006
+ const parts = ref.split(".");
2007
+ const binding = imports.find((candidate) => candidate.local === parts[0]);
2008
+ if (!binding)
2009
+ return ref;
2010
+ const importedBase = binding.kind === "module"
2011
+ ? binding.module
2012
+ : [binding.module, binding.imported].filter(Boolean).join(".");
2013
+ return [importedBase, ...parts.slice(1)].filter(Boolean).join(".");
2014
+ }
2015
+ function pythonMockedMethodReference(value) {
2016
+ return /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+$/.test(value) ? value : null;
2017
+ }
2018
+ /**
2019
+ * Exact patch/replacement targets only. This is intentionally syntax-only: the
2020
+ * analyzer resolves these references to an in-repo method before suppressing a
2021
+ * proof edge, so a dependency mock or unrelated class attribute remains inert.
2022
+ */
2023
+ function pythonMockedMethods(root, imports) {
2024
+ const out = new Set();
2025
+ const exactReceiverClass = (context, receiver) => {
2026
+ if (!/^[A-Za-z_]\w*$/.test(receiver))
2027
+ return null;
2028
+ let fn = context;
2029
+ while (fn && fn.type !== "function_definition")
2030
+ fn = fn.parent;
2031
+ const body = fn?.childForFieldName("body");
2032
+ if (!body)
2033
+ return null;
2034
+ const refs = pythonLexicalNodes(body)
2035
+ .filter((node) => node.type === "assignment" || node.type === "annotated_assignment")
2036
+ .map(pythonDirectAssignment)
2037
+ .filter((assignment) => Boolean(assignment?.target.type === "identifier" && assignment.target.text === receiver))
2038
+ .map((assignment) => pythonClassRefFromConstructor(assignment.value))
2039
+ .filter((ref) => Boolean(ref));
2040
+ return refs.length === 1 ? refs[0] : null;
2041
+ };
2042
+ const canonicalReceiver = (context, receiver) => exactReceiverClass(context, receiver) ?? receiver;
2043
+ const addObjectMethod = (context, object, method) => {
2044
+ const rawReceiver = pythonDottedName(object);
2045
+ const receiver = rawReceiver ? canonicalReceiver(context, rawReceiver) : null;
2046
+ const name = pythonStaticString(method);
2047
+ const ref = receiver && name && /^[A-Za-z_]\w*$/.test(name) ? `${receiver}.${name}` : null;
2048
+ if (ref)
2049
+ out.add(ref);
2050
+ };
2051
+ for (const call of root.descendantsOfType("call")) {
2052
+ if (!call)
2053
+ continue;
2054
+ const fn = pythonDottedName(call.childForFieldName("function"));
2055
+ const canonicalFn = pythonCanonicalImportedRef(fn, imports);
2056
+ const args = pythonPositionalArguments(call);
2057
+ if (canonicalFn === "unittest.mock.patch.object" || fn === "mocker.patch.object") {
2058
+ addObjectMethod(call, args[0] ?? null, args[1] ?? null);
2059
+ continue;
2060
+ }
2061
+ if (fn === "setattr" || fn === "monkeypatch.setattr") {
2062
+ // pytest's two-argument form accepts one fully qualified dotted target:
2063
+ // `monkeypatch.setattr("package.module.Class.method", replacement)`.
2064
+ const dotted = pythonStaticString(args[0] ?? null);
2065
+ const exact = dotted ? pythonMockedMethodReference(dotted) : null;
2066
+ if (exact)
2067
+ out.add(exact);
2068
+ else
2069
+ addObjectMethod(call, args[0] ?? null, args[1] ?? null);
2070
+ continue;
2071
+ }
2072
+ if (canonicalFn === "unittest.mock.patch" || fn === "mocker.patch") {
2073
+ const ref = pythonStaticString(args[0] ?? null);
2074
+ const exact = ref ? pythonMockedMethodReference(ref) : null;
2075
+ if (exact)
2076
+ out.add(exact);
2077
+ }
2078
+ }
2079
+ for (const assignment of root.descendantsOfType("assignment").filter((node) => Boolean(node))) {
2080
+ const rawTarget = pythonDottedName(assignment.childForFieldName("left"));
2081
+ const targetParts = rawTarget?.split(".") ?? [];
2082
+ const target = targetParts.length > 1
2083
+ ? [canonicalReceiver(assignment, targetParts[0]), ...targetParts.slice(1)].join(".")
2084
+ : rawTarget;
2085
+ const value = pythonExactCall(assignment.childForFieldName("right"));
2086
+ const mockFactory = value ? pythonCanonicalImportedRef(pythonDottedName(value.childForFieldName("function")), imports) : undefined;
2087
+ if (target && /^(?:unittest\.mock|mocker)\.(?:Mock|MagicMock|AsyncMock)$/.test(mockFactory ?? "")) {
2088
+ const exact = pythonMockedMethodReference(target);
2089
+ if (exact)
2090
+ out.add(exact);
2091
+ }
2092
+ }
2093
+ return [...out].sort();
2094
+ }
2095
+ /** Same-file pytest fixture dependencies are traversed by name, not by file membership. */
2096
+ function pythonScopedMockMethods(root, imports) {
2097
+ const fixtures = new Map();
2098
+ const tests = [];
2099
+ const decoratorsOf = (node) => node.parent?.type === "decorated_definition"
2100
+ ? namedChildren(node.parent).filter((child) => child.type === "decorator") : [];
2101
+ const params = (fn) => {
2102
+ const node = fn.childForFieldName("parameters");
2103
+ if (!node)
2104
+ return [];
2105
+ return namedChildren(node).flatMap((param) => {
2106
+ if (param.type === "identifier")
2107
+ return [param.text];
2108
+ if (["typed_parameter", "default_parameter", "typed_default_parameter"].includes(param.type)) {
2109
+ const name = param.childForFieldName("name") ?? namedChildren(param)[0];
2110
+ return name?.type === "identifier" ? [name.text] : [];
2111
+ }
2112
+ return [];
2113
+ }).filter((name) => name !== "self" && name !== "cls");
2114
+ };
2115
+ const finiteParametrizedNames = (decorators, parameter) => {
2116
+ const parametrizations = decorators.map((decorator) => decorator.text.trim())
2117
+ .filter((text) => text.startsWith("@pytest.mark.parametrize("));
2118
+ if (parametrizations.length !== 1)
2119
+ return null;
2120
+ const match = /^@pytest\.mark\.parametrize\(\s*(['"])([A-Za-z_]\w*)\1\s*,\s*\[([\s\S]*?)\]\s*,?\s*\)$/s.exec(parametrizations[0]);
2121
+ if (!match || match[2] !== parameter)
2122
+ return null;
2123
+ const values = match[3].split(",").map((part) => part.trim()).filter(Boolean);
2124
+ if (values.length === 0)
2125
+ return null;
2126
+ const names = values.map((part) => /^(['"])([A-Za-z_]\w*)\1$/.exec(part)?.[2] ?? null);
2127
+ return names.every((name) => Boolean(name)) ? names : null;
2128
+ };
2129
+ const dynamic = (fn, decorators = []) => {
2130
+ const names = [];
2131
+ let unknown = false;
2132
+ for (const call of fn.descendantsOfType("call")) {
2133
+ if (!call || pythonDottedName(call.childForFieldName("function")) !== "request.getfixturevalue")
2134
+ continue;
2135
+ const args = pythonPositionalArguments(call);
2136
+ const name = args.length === 1 ? pythonStaticString(args[0] ?? null) : null;
2137
+ if (name && /^[A-Za-z_]\w*$/.test(name))
2138
+ names.push(name);
2139
+ else {
2140
+ const parameter = args.length === 1 && args[0]?.type === "identifier" ? args[0].text : null;
2141
+ const selected = parameter && params(fn).includes("request") && params(fn).includes(parameter)
2142
+ ? finiteParametrizedNames(decorators, parameter) : null;
2143
+ if (selected)
2144
+ names.push(...selected);
2145
+ else
2146
+ unknown = true;
2147
+ }
2148
+ }
2149
+ return { names, unknown };
2150
+ };
2151
+ const usefixtures = (decorators) => {
2152
+ const names = [];
2153
+ let unknown = false;
2154
+ for (const decorator of decorators) {
2155
+ const text = decorator.text.trim();
2156
+ if (!/^@pytest\.mark\.usefixtures\(/.test(text))
2157
+ continue;
2158
+ const match = text.match(/^@pytest\.mark\.usefixtures\((.*)\)$/s);
2159
+ if (!match) {
2160
+ unknown = true;
2161
+ continue;
2162
+ }
2163
+ names.push(...[...match[1].matchAll(/(['"])([A-Za-z_]\w*)\1/g)].map((value) => value[2]));
2164
+ unknown ||= Boolean(match[1].replace(/(['"])[A-Za-z_]\w*\1/g, "").replace(/[\s,]/g, ""));
2165
+ }
2166
+ return { names, unknown };
2167
+ };
2168
+ for (const fn of root.descendantsOfType("function_definition")) {
2169
+ if (!fn || pythonNearestFunction(fn))
2170
+ continue;
2171
+ const name = fn.childForFieldName("name")?.text;
2172
+ if (!name)
2173
+ continue;
2174
+ const enclosing = fn.parent?.type === "decorated_definition" ? fn.parent : fn;
2175
+ const classNode = enclosing.parent?.type === "block" && enclosing.parent.parent?.type === "class_definition" ? enclosing.parent.parent : null;
2176
+ const owner = classNode?.childForFieldName("name")?.text ?? null;
2177
+ const decorators = decoratorsOf(fn);
2178
+ const fixtureDecorator = decorators.find((decorator) => /^@pytest\.fixture(?:\(|$)/.test(decorator.text.trim()) ||
2179
+ (/^@fixture(?:\(|$)/.test(decorator.text.trim()) && imports.some((binding) => binding.local === "fixture" && binding.module === "pytest" && binding.imported === "fixture")));
2180
+ if (fixtureDecorator) {
2181
+ const text = fixtureDecorator.text.trim();
2182
+ const alias = text.match(/\bname\s*=\s*(['"])([A-Za-z_]\w*)\1/);
2183
+ const fixtureName = alias?.[2] ?? name;
2184
+ const selected = dynamic(fn);
2185
+ fixtures.set(`${owner ?? ""}::${fixtureName}`, {
2186
+ owner, name: fixtureName, mocks: [...new Set([
2187
+ ...pythonMockedMethods(fn, imports),
2188
+ ...decorators.flatMap((decorator) => pythonMockedMethods(decorator, imports))
2189
+ ])],
2190
+ dependencies: [...params(fn), ...selected.names], autouse: /\bautouse\s*=\s*True\b/.test(text), unknown: selected.unknown
2191
+ });
2192
+ }
2193
+ if (/^test_[A-Za-z0-9_]*$/.test(name))
2194
+ tests.push({
2195
+ name: owner ? `${owner}::${name}` : name, fn, owner,
2196
+ decorators: [...decorators, ...(classNode ? decoratorsOf(classNode) : [])]
2197
+ });
2198
+ }
2199
+ const moduleMocks = namedChildren(root)
2200
+ .filter((node) => !["function_definition", "class_definition", "decorated_definition"].includes(node.type))
2201
+ .flatMap((node) => pythonMockedMethods(node, imports));
2202
+ const byTest = {};
2203
+ const unknown = [];
2204
+ for (const test of tests) {
2205
+ const mocks = new Set([...moduleMocks, ...pythonMockedMethods(test.fn, imports)]);
2206
+ for (const decorator of test.decorators)
2207
+ for (const ref of pythonMockedMethods(decorator, imports))
2208
+ mocks.add(ref);
2209
+ const selected = usefixtures(test.decorators);
2210
+ const requested = dynamic(test.fn, test.decorators);
2211
+ let uncertain = selected.unknown || requested.unknown;
2212
+ for (const name of requested.names) {
2213
+ if (!fixtures.has(`${test.owner ?? ""}::${name}`) && !fixtures.has(`::${name}`))
2214
+ uncertain = true;
2215
+ }
2216
+ const seen = new Set();
2217
+ const visit = (name) => {
2218
+ const fixture = fixtures.get(`${test.owner ?? ""}::${name}`) ?? fixtures.get(`::${name}`);
2219
+ if (!fixture)
2220
+ return;
2221
+ const key = `${fixture.owner ?? ""}::${fixture.name}`;
2222
+ if (seen.has(key))
2223
+ return;
2224
+ seen.add(key);
2225
+ uncertain ||= fixture.unknown;
2226
+ for (const ref of fixture.mocks)
2227
+ mocks.add(ref);
2228
+ for (const dependency of fixture.dependencies)
2229
+ visit(dependency);
2230
+ };
2231
+ for (const name of [...params(test.fn), ...selected.names, ...requested.names])
2232
+ visit(name);
2233
+ for (const fixture of fixtures.values())
2234
+ if (fixture.autouse && (!fixture.owner || fixture.owner === test.owner))
2235
+ visit(fixture.name);
2236
+ byTest[test.name] = [...mocks].sort();
2237
+ if (uncertain)
2238
+ unknown.push(test.name);
2239
+ }
2240
+ return { byTest, unknown };
2241
+ }
2242
+ function pythonFixtureBindings(root) {
2243
+ const out = new Map();
2244
+ for (const decorated of root.descendantsOfType("decorated_definition")) {
2245
+ if (!decorated || decorated.parent?.type !== "module")
2246
+ continue;
2247
+ if (!namedChildren(decorated).some((child) => child.type === "decorator" && pythonFixtureDecorator(child)))
2248
+ continue;
2249
+ const fn = namedChildren(decorated).find((child) => child.type === "function_definition");
2250
+ const name = fn?.childForFieldName("name")?.text;
2251
+ const body = fn?.childForFieldName("body");
2252
+ if (!name || !body)
2253
+ continue;
2254
+ const values = blockStatements(body).flatMap((statement) => {
2255
+ if (statement.type === "return_statement")
2256
+ return [statement.namedChild(0) ?? null];
2257
+ if (statement.type === "expression_statement" && statement.namedChild(0)?.type === "yield")
2258
+ return [statement.namedChild(0)?.namedChild(0) ?? null];
2259
+ return [];
2260
+ });
2261
+ const direct = values.length === 1 ? pythonClassRefFromConstructor(values[0] ?? null) : null;
2262
+ const directRule = values.length === 1 && values[0]?.parent?.type === "return_statement"
2263
+ ? "fixture_return"
2264
+ : values.length === 1 ? "fixture_yield" : null;
2265
+ const annotated = pythonTypeRef(fn.childForFieldName("return_type"));
2266
+ if (direct && annotated && direct !== annotated)
2267
+ continue;
2268
+ if (direct && directRule)
2269
+ out.set(name, { classRef: direct, rule: directRule });
2270
+ else if (!direct && values.length === 0 && annotated)
2271
+ out.set(name, { classRef: annotated, rule: "fixture_annotation" });
2272
+ }
2273
+ return out;
2274
+ }
2275
+ function pythonTestFixtureReceiver(testFunction, fixtures) {
2276
+ const parameters = testFunction?.childForFieldName("parameters");
2277
+ if (!parameters)
2278
+ return new Map();
2279
+ const values = namedChildren(parameters).filter((node) => node.type === "identifier" || node.type === "typed_parameter");
2280
+ const receivers = new Map();
2281
+ for (const parameter of values) {
2282
+ const name = parameter.type === "identifier" ? parameter.text : parameter.childForFieldName("name")?.text ?? parameter.namedChild(0)?.text;
2283
+ if (!name || name === "self" || name === "cls")
2284
+ continue;
2285
+ const fixture = fixtures.get(name);
2286
+ const annotation = parameter.type === "typed_parameter" ? pythonTypeRef(parameter.childForFieldName("type")) : null;
2287
+ // A pytest parameter annotated with one plain class reference is a structural
2288
+ // fixture/type producer in its own right (the provider may live in conftest).
2289
+ // If an in-file fixture exists it must agree exactly; disagreement is unsafe.
2290
+ if (!fixture && !annotation)
2291
+ continue;
2292
+ if (fixture && annotation && annotation !== fixture.classRef)
2293
+ return null;
2294
+ if (receivers.size > 0)
2295
+ return null; // one unambiguous fixture receiver only
2296
+ receivers.set(name, { classRef: annotation ?? fixture.classRef, rule: annotation ? "parameter_annotation" : fixture.rule, assignedAt: -1 });
2297
+ }
2298
+ return receivers;
2299
+ }
2300
+ function pythonSetupReceivers(classNode) {
2301
+ const out = new Map();
2302
+ if (!classNode)
2303
+ return out;
2304
+ const body = classNode.childForFieldName("body");
2305
+ if (!body)
2306
+ return out;
2307
+ for (const method of namedChildren(body).filter((node) => node.type === "function_definition")) {
2308
+ const methodName = method.childForFieldName("name")?.text;
2309
+ if (methodName !== "setUp" && methodName !== "setup_method")
2310
+ continue;
2311
+ const methodBody = method.childForFieldName("body");
2312
+ if (!methodBody)
2313
+ return null;
2314
+ for (const statement of blockStatements(methodBody)) {
2315
+ const assignment = pythonDirectAssignment(statement);
2316
+ if (!assignment || assignment.target.type !== "attribute")
2317
+ continue;
2318
+ const receiver = pythonReceiverKey(assignment.target);
2319
+ const classRef = pythonClassRefFromConstructor(assignment.value);
2320
+ // Setup frequently initializes unrelated module state (`settings.flag = ...`)
2321
+ // or scalar test state (`self.started_at = ...`). Neither establishes a
2322
+ // receiver, so it must not veto a separate exact local-instance binding.
2323
+ // Once a receiver is established, however, an unsupported reassignment or
2324
+ // nested attribute write still invalidates that receiver conservatively.
2325
+ if (!receiver) {
2326
+ const target = pythonDottedName(assignment.target);
2327
+ if (target && [...out.keys()].some((known) => target.startsWith(`${known}.`)))
2328
+ return null;
2329
+ continue;
2330
+ }
2331
+ if (!classRef) {
2332
+ if (out.has(receiver))
2333
+ return null;
2334
+ continue;
2335
+ }
2336
+ if (out.has(receiver))
2337
+ return null;
2338
+ out.set(receiver, { classRef, rule: "unittest_setup", assignedAt: assignment.target.startIndex });
2339
+ }
2340
+ }
2341
+ return out;
2342
+ }
2343
+ function pythonStructuralReceiver(callNode, receivers) {
2344
+ const method = pythonMethodCall(callNode);
2345
+ if (!method)
2346
+ return null;
2347
+ const bound = receivers.get(pythonReceiverKey(method.receiver) ?? "");
2348
+ if (bound)
2349
+ return bound;
2350
+ const inline = pythonClassRefFromConstructor(method.receiver);
2351
+ if (inline)
2352
+ return { classRef: inline, rule: "inline_constructor", assignedAt: method.receiver.startIndex };
2353
+ const classRef = pythonDottedName(method.receiver);
2354
+ // A lower-case receiver is an ordinary module/object call (`calc.add()`), not
2355
+ // a structural class/static receiver. Retain the existing exact module-function
2356
+ // lane for those calls rather than guessing that any attribute is a class method.
2357
+ return classRef && pythonLooksLikeClassRef(classRef)
2358
+ ? { classRef, rule: "class_static_method", assignedAt: method.receiver.startIndex }
2359
+ : null;
2360
+ }
2361
+ /** True when an asserted call uses a receiver form owned by the structural lane. */
2362
+ function pythonStructuralReceiverSyntax(callNode) {
2363
+ const method = pythonMethodCall(callNode);
2364
+ if (!method)
2365
+ return false;
2366
+ // A lower-case receiver may be an imported module (`calc.add()`), which is
2367
+ // owned by the legacy exact module-import lane. Stored/local receivers still
2368
+ // pass through that lane harmlessly (they cannot resolve as a module/class)
2369
+ // when the structural association guards decline them.
2370
+ const classRef = pythonDottedName(method.receiver);
2371
+ return Boolean(classRef && pythonLooksLikeClassRef(classRef));
2372
+ }
2373
+ function pythonMentionsReceiverInArguments(callNode, receiver) {
2374
+ const args = callNode.childForFieldName("arguments");
2375
+ return args ? pythonMentionsReceiver(args, receiver) : false;
2376
+ }
2377
+ /** Exact receiver reference in an expression; strings/comments never constitute escape. */
2378
+ function pythonMentionsReceiver(node, receiver) {
2379
+ const candidates = [node, ...node.descendantsOfType("identifier"), ...node.descendantsOfType("attribute")];
2380
+ return candidates.some((candidate) => pythonReceiverKey(candidate) === receiver);
2381
+ }
2382
+ function pythonStructuralAssociationProofCalls(block, testFunction, imports, fixtures, setup) {
2383
+ const fixtureReceivers = pythonTestFixtureReceiver(testFunction, fixtures);
2384
+ if (!fixtureReceivers || setup === null)
2385
+ return [];
2386
+ const lexicalNodes = pythonLexicalNodes(block);
2387
+ const assignments = lexicalNodes
2388
+ .filter((node) => node.type === "assignment" || node.type === "annotated_assignment")
2389
+ .map(pythonDirectAssignment)
2390
+ .filter((v) => Boolean(v));
2391
+ const assertions = lexicalNodes.filter((node) => node.type === "assert_statement");
2392
+ const receivers = new Map();
2393
+ for (const [name, binding] of fixtureReceivers)
2394
+ receivers.set(name, binding);
2395
+ for (const [name, binding] of setup) {
2396
+ if (receivers.has(name))
2397
+ return [];
2398
+ receivers.set(name, binding);
2399
+ }
2400
+ const writes = new Map();
2401
+ for (const { target } of assignments) {
2402
+ const key = pythonReceiverKey(target);
2403
+ if (key)
2404
+ writes.set(key, (writes.get(key) ?? 0) + 1);
2405
+ }
2406
+ for (const { target, value } of assignments) {
2407
+ if (target.type !== "identifier")
2408
+ continue;
2409
+ const classRef = pythonClassRefFromConstructor(value);
2410
+ if (!classRef || (writes.get(target.text) ?? 0) !== 1 || receivers.has(target.text))
2411
+ continue;
2412
+ receivers.set(target.text, { classRef, rule: classRef.includes(".") ? "module_assigned_instance" : "assigned_instance", assignedAt: target.startIndex });
2413
+ }
2414
+ for (const { target } of assignments) {
2415
+ if (target.type !== "identifier")
2416
+ continue;
2417
+ if ([...receivers.values()].some((receiver) => receiver.classRef === target.text))
2418
+ return [];
2419
+ }
2420
+ // Any write/alias to a tracked receiver, or passing it to a helper/setter, is
2421
+ // receiver escape. Attribute replacement is narrower: it invalidates only that
2422
+ // exact receiver method, so mocking one dependency hook cannot erase an unrelated
2423
+ // invocation on the same production object.
2424
+ const replacedMethods = new Set();
2425
+ for (const { target, value } of assignments) {
2426
+ const targetKey = pythonReceiverKey(target);
2427
+ if (targetKey && receivers.has(targetKey) && !(target.type === "identifier" && receivers.get(targetKey)?.assignedAt === target.startIndex))
2428
+ return [];
2429
+ if (target.type === "attribute") {
2430
+ const dotted = pythonDottedName(target);
2431
+ if (dotted) {
2432
+ for (const receiver of receivers.keys()) {
2433
+ if (dotted.startsWith(`${receiver}.`))
2434
+ replacedMethods.add(dotted);
2435
+ }
2436
+ }
2437
+ }
2438
+ if (target.type === "identifier" && (receivers.has(value.text) || receivers.has(pythonReceiverKey(value) ?? "")))
2439
+ return [];
2440
+ const exactReceiverMethodResult = (() => {
2441
+ const call = pythonExactCall(value);
2442
+ const method = call ? pythonMethodCall(call) : null;
2443
+ const receiver = method ? pythonReceiverKey(method.receiver) : null;
2444
+ return Boolean(receiver && receivers.has(receiver));
2445
+ })();
2446
+ if (target.type === "identifier" &&
2447
+ !exactReceiverMethodResult &&
2448
+ [...receivers.keys()].some((receiver) => pythonMentionsReceiver(value, receiver)))
2449
+ return [];
2450
+ }
2451
+ for (const mutation of lexicalNodes.filter((node) => node.type === "augmented_assignment")) {
2452
+ if ([...receivers.keys()].some((receiver) => new RegExp(`(^|[^A-Za-z0-9_])${receiver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:$|[^A-Za-z0-9_])`).test(mutation.text)))
2453
+ return [];
2454
+ }
2455
+ for (const deleted of lexicalNodes.filter((node) => node.type === "delete_statement")) {
2456
+ if ([...receivers.keys()].some((receiver) => new RegExp(`(^|[^A-Za-z0-9_])${receiver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:$|[^A-Za-z0-9_])`).test(deleted.text)))
2457
+ return [];
2458
+ }
2459
+ for (const callNode of lexicalNodes.filter((node) => node.type === "call")) {
2460
+ const method = pythonMethodCall(callNode);
2461
+ if (method && pythonStructuralReceiver(callNode, receivers))
2462
+ continue;
2463
+ for (const receiver of receivers.keys())
2464
+ if (pythonMentionsReceiverInArguments(callNode, receiver))
2465
+ return [];
2466
+ }
2467
+ const relevant = lexicalNodes
2468
+ .filter((node) => node.type === "call")
2469
+ .map((node) => ({ node, method: pythonMethodCall(node), receiver: pythonStructuralReceiver(node, receivers) }))
2470
+ .filter((v) => {
2471
+ if (!v.method || !v.receiver)
2472
+ return false;
2473
+ const receiver = pythonReceiverKey(v.method.receiver);
2474
+ return !receiver || !replacedMethods.has(`${receiver}.${v.method.call.callee}`);
2475
+ });
2476
+ if (relevant.length > 0) {
2477
+ // Exact constructor/type/setup/class origins make every direct invocation an
2478
+ // Association. Assertion count and result plumbing are proof concerns, not
2479
+ // association concerns. Analyzer-side class/MRO resolution still fails closed.
2480
+ return relevant.map((target) => ({
2481
+ assertion: assertions[0] ?? target.node,
2482
+ call: target.method.call,
2483
+ instanceClass: target.receiver.classRef,
2484
+ associationRule: target.receiver.rule
2485
+ }));
2486
+ }
2487
+ // Preserve the deliberately narrow unresolved-receiver fallback. Unlike exact
2488
+ // constructor/type receivers, it still requires one assertion and one call.
2489
+ if (assertions.length !== 1)
2490
+ return [];
2491
+ const testParameters = new Set();
2492
+ const parameters = testFunction.childForFieldName("parameters");
2493
+ for (const parameter of parameters ? namedChildren(parameters) : []) {
2494
+ const name = parameter.type === "identifier" ? parameter.text : parameter.childForFieldName("name")?.text ?? parameter.namedChild(0)?.text;
2495
+ if (name)
2496
+ testParameters.add(name);
2497
+ }
2498
+ // The only deliberately unresolved lane: one lower-case local receiver, one
2499
+ // direct method invocation, and the same one-assertion/result guards above.
2500
+ // Capitalized class syntax, inline constructors, fixtures, setup properties,
2501
+ // and any otherwise resolved receiver were handled by `relevant` above and
2502
+ // therefore cannot reach this lane.
2503
+ const unresolved = lexicalNodes
2504
+ .filter((node) => node.type === "call")
2505
+ .map((node) => ({ node, method: pythonMethodCall(node) }))
2506
+ .filter((v) => Boolean(v.method &&
2507
+ v.method.receiver.type === "identifier" &&
2508
+ /^[a-z_][A-Za-z0-9_]*$/.test(v.method.receiver.text) &&
2509
+ v.method.receiver.text !== "self" &&
2510
+ v.method.receiver.text !== "cls" &&
2511
+ !testParameters.has(v.method.receiver.text) &&
2512
+ // An imported lower-case name can be an exact module receiver
2513
+ // (`calc.add()`); leave that to the legacy import-resolution lane.
2514
+ !imports.some((binding) => binding.local === v.method?.receiver.text)));
2515
+ if (unresolved.length !== 1)
2516
+ return [];
2517
+ const target = unresolved[0];
2518
+ const results = new Map();
2519
+ for (const { target: resultName, value } of assignments) {
2520
+ if (resultName.type !== "identifier" || writes.get(resultName.text) !== 1)
2521
+ continue;
2522
+ const call = pythonExactCall(value);
2523
+ if (call?.id === target.node.id)
2524
+ results.set(resultName.text, target);
2525
+ }
2526
+ const direct = directPythonAssertCallNode(assertions[0]);
2527
+ if (direct?.id === target.node.id) {
2528
+ return [{ assertion: assertions[0], call: target.method.call, associationRule: "unique_method_name_import" }];
2529
+ }
2530
+ const observed = pythonDirectAssertedResult(assertions[0], results);
2531
+ const result = observed ? results.get(observed) : undefined;
2532
+ return result
2533
+ ? [{ assertion: assertions[0], call: result.method.call, associationRule: "unique_method_name_import" }]
2534
+ : [];
2535
+ }
2536
+ function extractPythonProofCalls(root, imports) {
1860
2537
  const out = [];
1861
2538
  const seen = new Set();
1862
- const add = (testName, shadowed, call) => {
2539
+ const fixtures = pythonFixtureBindings(root);
2540
+ const add = (testName, shadowed, call, instanceClass, associationRule) => {
1863
2541
  if (!call)
1864
2542
  return;
1865
- const key = `${testName}|${call.qualifier ?? ""}|${call.callee}`;
2543
+ const key = `${testName}|${instanceClass ?? ""}|${call.qualifier ?? ""}|${call.callee}`;
1866
2544
  if (seen.has(key))
1867
2545
  return;
1868
2546
  seen.add(key);
1869
- out.push({ caller: testName, testName, ...call, shadowed: [...shadowed], assertion: "pytest_assert" });
2547
+ out.push({
2548
+ caller: testName,
2549
+ testName,
2550
+ ...call,
2551
+ shadowed: [...shadowed],
2552
+ assertion: "pytest_assert",
2553
+ ...(instanceClass ? { instanceClass } : {}),
2554
+ ...(associationRule ? { associationRule } : {})
2555
+ });
1870
2556
  };
1871
- const processBlock = (block, testName, shadowed) => {
2557
+ const processBlock = (block, testFunction, testName, shadowed, setup) => {
2558
+ // A direct `ImportedClass.method(...)` call is invocation-backed Association
2559
+ // even when the test stores its result, has multiple assertions, or mocks an
2560
+ // unrelated dependency. Analyzer-side resolution still requires an exact
2561
+ // in-repo class import and a declared @classmethod/@staticmethod, and exact
2562
+ // target mocks are suppressed before any hard edge is emitted.
2563
+ for (const callNode of pythonLexicalNodes(block).filter((node) => node.type === "call")) {
2564
+ const method = pythonMethodCall(callNode);
2565
+ if (!method)
2566
+ continue;
2567
+ const classRef = pythonDottedName(method.receiver);
2568
+ if (classRef && pythonLooksLikeClassRef(classRef)) {
2569
+ add(testName, shadowed, method.call, classRef, "class_static_method");
2570
+ }
2571
+ }
2572
+ const associatedCalls = pythonStructuralAssociationProofCalls(block, testFunction, imports, fixtures, setup);
2573
+ for (const associated of associatedCalls) {
2574
+ add(testName, shadowed, associated.call, associated.instanceClass, associated.associationRule);
2575
+ }
1872
2576
  for (const stmt of blockStatements(block)) {
1873
- if (stmt.type === "assert_statement")
1874
- add(testName, shadowed, directPythonAssertCall(stmt));
2577
+ if (stmt.type === "assert_statement") {
2578
+ const directNode = directPythonAssertCallNode(stmt);
2579
+ const direct = directPythonAssertCall(stmt);
2580
+ // Structural receiver syntax must be resolved only by the static-association
2581
+ // lane above. The legacy direct-call lane does not know MRO/decorators and
2582
+ // would otherwise turn `Cls.instance_method()` into a false confirmation.
2583
+ const structurallyAssociated = Boolean(direct && associatedCalls.some((associated) =>
2584
+ // Preserve the legacy same-package convention path for inline constructors
2585
+ // with no explicit import. The structural class origin is exact, but the
2586
+ // analyzer cannot resolve that class owner from this association alone.
2587
+ associated.associationRule !== "inline_constructor" &&
2588
+ associated.call.callee === direct.callee &&
2589
+ associated.call.qualifier === direct.qualifier &&
2590
+ associated.call.via === direct.via));
2591
+ if ((!directNode || !pythonStructuralReceiverSyntax(directNode)) && !structurallyAssociated)
2592
+ add(testName, shadowed, direct);
2593
+ }
1875
2594
  if (stmt.type === "function_definition" || stmt.type === "class_definition" || stmt.type === "lambda")
1876
2595
  continue;
1877
2596
  for (const child of namedChildren(stmt)) {
1878
2597
  if (child.type === "block")
1879
- processBlock(child, testName, shadowed);
2598
+ processBlock(child, testFunction, testName, shadowed, setup);
1880
2599
  }
1881
2600
  }
1882
2601
  };
1883
2602
  const walk = (node, insideFunction, testClass) => {
1884
2603
  if (!insideFunction && node.type === "class_definition") {
1885
2604
  const name = node.childForFieldName("name")?.text;
1886
- const nextTestClass = name && /^Test[A-Za-z0-9_]*$/.test(name) ? name : null;
2605
+ const superclasses = node.childForFieldName("superclasses");
2606
+ const bases = superclasses ? namedChildren(superclasses).map((base) => base.text) : [];
2607
+ const isTestClass = Boolean(name && (/^Test[A-Za-z0-9_]*$/.test(name) || bases.some((base) => /(?:^|\.)TestCase$/.test(base))));
2608
+ const nextTestClass = name && isTestClass ? { name, node } : null;
1887
2609
  for (const child of namedChildren(node))
1888
2610
  walk(child, false, nextTestClass);
1889
2611
  return;
@@ -1892,7 +2614,7 @@ function extractPythonProofCalls(root) {
1892
2614
  const name = functionName(node, "python");
1893
2615
  const body = node.childForFieldName("body");
1894
2616
  if (name && /^test_[A-Za-z0-9_]*$/.test(name) && body) {
1895
- processBlock(body, testClass ? `${testClass}::${name}` : name, localBindings(node, "python"));
2617
+ processBlock(body, node, testClass ? `${testClass.name}::${name}` : name, localBindings(node, "python"), pythonSetupReceivers(testClass?.node ?? null));
1896
2618
  }
1897
2619
  if (insideFunction)
1898
2620
  return;
@@ -1903,6 +2625,415 @@ function extractPythonProofCalls(root) {
1903
2625
  walk(root, false, null);
1904
2626
  return out;
1905
2627
  }
2628
+ function pythonClassInfos(root) {
2629
+ const out = [];
2630
+ for (const node of root.descendantsOfType("class_definition")) {
2631
+ if (!node || hasPythonFunctionAncestor(node))
2632
+ continue;
2633
+ const name = node.childForFieldName("name")?.text;
2634
+ const superclasses = node.childForFieldName("superclasses");
2635
+ const bases = superclasses
2636
+ ? namedChildren(superclasses).map(pythonDottedName).filter((base) => Boolean(base))
2637
+ : [];
2638
+ const body = node.childForFieldName("body");
2639
+ const classStaticMethods = body
2640
+ ? namedChildren(body)
2641
+ .filter((member) => member.type === "decorated_definition")
2642
+ .filter((member) => namedChildren(member).some((child) => child.type === "decorator" && /^@(?:class|static)method$/.test(child.text.trim())))
2643
+ .map((member) => namedChildren(member).find((child) => child.type === "function_definition"))
2644
+ .map((method) => method?.childForFieldName("name")?.text)
2645
+ .filter((method) => Boolean(method))
2646
+ : [];
2647
+ if (name)
2648
+ out.push({ name, bases, classStaticMethods: [...new Set(classStaticMethods)].sort() });
2649
+ }
2650
+ return out;
2651
+ }
2652
+ const PYTHON_ENUM_TYPES = new Set(["Enum", "IntEnum", "StrEnum", "Flag", "IntFlag"]);
2653
+ /** Import bindings that are still exact immediately before one direct module statement. */
2654
+ function pythonImportsBefore(root, stopAt) {
2655
+ const bindings = new Map();
2656
+ const invalidate = (name) => { if (name)
2657
+ bindings.delete(name); };
2658
+ for (const statement of namedChildren(root)) {
2659
+ if (statement.startIndex >= stopAt)
2660
+ break;
2661
+ if (statement.type === "import_statement" || statement.type === "import_from_statement") {
2662
+ for (const binding of extractImports(statement, "python")) {
2663
+ bindings.set(binding.local, { module: binding.module, imported: binding.imported, kind: binding.kind });
2664
+ }
2665
+ continue;
2666
+ }
2667
+ if (statement.type === "function_definition" || statement.type === "class_definition") {
2668
+ invalidate(statement.childForFieldName("name")?.text ?? "");
2669
+ continue;
2670
+ }
2671
+ if (statement.type === "decorated_definition") {
2672
+ const declaration = namedChildren(statement).find((child) => child.type === "function_definition" || child.type === "class_definition");
2673
+ invalidate(declaration?.childForFieldName("name")?.text ?? "");
2674
+ continue;
2675
+ }
2676
+ if (statement.type === "assignment" || statement.type === "annotated_assignment" || statement.type === "augmented_assignment") {
2677
+ for (const name of pythonBindingTargetNames(statement.childForFieldName("left")))
2678
+ invalidate(name);
2679
+ continue;
2680
+ }
2681
+ if (statement.type === "delete_statement") {
2682
+ for (const child of namedChildren(statement))
2683
+ for (const name of pythonBindingTargetNames(child))
2684
+ invalidate(name);
2685
+ }
2686
+ }
2687
+ return bindings;
2688
+ }
2689
+ function pythonBaseIsStdlib(base, bindings, module, allowed) {
2690
+ const dotted = pythonDottedName(base);
2691
+ if (!dotted)
2692
+ return false;
2693
+ const parts = dotted.split(".");
2694
+ if (parts.length === 1) {
2695
+ const binding = bindings.get(parts[0]);
2696
+ return Boolean(binding?.kind === "named" && binding.module === module && binding.imported && allowed.has(binding.imported));
2697
+ }
2698
+ if (parts.length === 2 && allowed.has(parts[1])) {
2699
+ const binding = bindings.get(parts[0]);
2700
+ return Boolean(binding?.kind === "module" && binding.module === module);
2701
+ }
2702
+ return false;
2703
+ }
2704
+ function pythonClassHasMethod(node) {
2705
+ const body = node.childForFieldName("body");
2706
+ if (!body)
2707
+ return false;
2708
+ const visit = (current) => {
2709
+ if (current !== body && current.type === "class_definition")
2710
+ return false;
2711
+ if (current.type === "function_definition")
2712
+ return true;
2713
+ return namedChildren(current).some(visit);
2714
+ };
2715
+ return namedChildren(body).some(visit);
2716
+ }
2717
+ function pythonParameterNames(fn) {
2718
+ const params = fn.childForFieldName("parameters");
2719
+ if (!params)
2720
+ return null;
2721
+ const out = [];
2722
+ for (const parameter of namedChildren(params)) {
2723
+ if (parameter.type === "identifier") {
2724
+ out.push(parameter.text);
2725
+ continue;
2726
+ }
2727
+ if (parameter.type === "default_parameter" || parameter.type === "typed_parameter" || parameter.type === "typed_default_parameter") {
2728
+ const name = parameter.childForFieldName("name") ?? namedChildren(parameter)[0] ?? null;
2729
+ if (name?.type !== "identifier")
2730
+ return null;
2731
+ out.push(name.text);
2732
+ continue;
2733
+ }
2734
+ // Splat/pattern/separator shapes are not bare forwarding parameters.
2735
+ return null;
2736
+ }
2737
+ return out;
2738
+ }
2739
+ function pythonLiteralOrParameter(node, params) {
2740
+ if (!node)
2741
+ return false;
2742
+ if (node.type === "identifier")
2743
+ return params.has(node.text);
2744
+ return new Set(["none", "true", "false", "integer", "float", "string", "concatenated_string"]).has(node.type);
2745
+ }
2746
+ function pythonDocstringStatement(statement) {
2747
+ if (statement.type !== "expression_statement")
2748
+ return false;
2749
+ const expression = namedChildren(statement)[0];
2750
+ return expression?.type === "string" || expression?.type === "concatenated_string";
2751
+ }
2752
+ function pythonSuperInitStatement(statement) {
2753
+ if (statement.type !== "expression_statement")
2754
+ return false;
2755
+ const call = namedChildren(statement)[0];
2756
+ if (call?.type !== "call" || pythonCallArguments(call).length !== 0)
2757
+ return false;
2758
+ const fn = call.childForFieldName("function");
2759
+ if (fn?.type !== "attribute" || fn.childForFieldName("attribute")?.text !== "__init__")
2760
+ return false;
2761
+ const receiver = fn.childForFieldName("object");
2762
+ if (receiver?.type !== "call" || pythonCallArguments(receiver).length !== 0)
2763
+ return false;
2764
+ return receiver.childForFieldName("function")?.type === "identifier" && receiver.childForFieldName("function")?.text === "super";
2765
+ }
2766
+ function pythonTrivialSelfAssignment(statement, params) {
2767
+ if (statement.type !== "expression_statement")
2768
+ return false;
2769
+ const assignment = namedChildren(statement)[0];
2770
+ if (assignment?.type !== "assignment")
2771
+ return false;
2772
+ const left = assignment.childForFieldName("left");
2773
+ if (left?.type !== "attribute" || left.childForFieldName("object")?.type !== "identifier" || left.childForFieldName("object")?.text !== "self")
2774
+ return false;
2775
+ return pythonLiteralOrParameter(assignment.childForFieldName("right"), params);
2776
+ }
2777
+ function pythonTrivialThreadLocalInitializer(classNode) {
2778
+ const body = classNode.childForFieldName("body");
2779
+ if (!body)
2780
+ return null;
2781
+ const methods = namedChildren(body).flatMap((statement) => {
2782
+ if (statement.type === "function_definition")
2783
+ return [statement];
2784
+ if (statement.type === "decorated_definition") {
2785
+ const fn = namedChildren(statement).find((child) => child.type === "function_definition");
2786
+ return fn ? [fn] : [];
2787
+ }
2788
+ return [];
2789
+ }).filter((fn) => fn.childForFieldName("name")?.text === "__init__");
2790
+ if (methods.length !== 1)
2791
+ return null;
2792
+ const fn = methods[0];
2793
+ const paramsList = pythonParameterNames(fn);
2794
+ const fnBody = fn.childForFieldName("body");
2795
+ if (!paramsList || !fnBody)
2796
+ return null;
2797
+ const params = new Set(paramsList);
2798
+ const statements = blockStatements(fnBody);
2799
+ for (let i = 0; i < statements.length; i++) {
2800
+ const statement = statements[i];
2801
+ if (i === 0 && pythonDocstringStatement(statement))
2802
+ continue;
2803
+ if (statement.type === "pass_statement" || pythonSuperInitStatement(statement) || pythonTrivialSelfAssignment(statement, params))
2804
+ continue;
2805
+ return null;
2806
+ }
2807
+ return pythonQualifiedFunctionName(fn) ?? null;
2808
+ }
2809
+ function extractPythonDeclarationRankingExclusions(root) {
2810
+ const out = [];
2811
+ for (const classNode of root.descendantsOfType("class_definition")) {
2812
+ if (!classNode || hasPythonFunctionAncestor(classNode) || classNode.parent?.type !== "module")
2813
+ continue;
2814
+ const name = classNode.childForFieldName("name")?.text;
2815
+ const bases = classNode.childForFieldName("superclasses");
2816
+ if (!name || !bases)
2817
+ continue;
2818
+ const bindings = pythonImportsBefore(root, classNode.startIndex);
2819
+ const baseNodes = namedChildren(bases);
2820
+ if (baseNodes.some((base) => pythonBaseIsStdlib(base, bindings, "enum", PYTHON_ENUM_TYPES)) && !pythonClassHasMethod(classNode)) {
2821
+ out.push({
2822
+ symbol: name,
2823
+ code: "python_stdlib_enum_declaration",
2824
+ reason: "Standard-library enum declaration without methods — retained in the graph but excluded from priority ranking."
2825
+ });
2826
+ }
2827
+ if (baseNodes.some((base) => pythonBaseIsStdlib(base, bindings, "threading", new Set(["local"])))) {
2828
+ const initializer = pythonTrivialThreadLocalInitializer(classNode);
2829
+ if (initializer) {
2830
+ out.push({
2831
+ symbol: initializer,
2832
+ code: "python_trivial_thread_local_initializer",
2833
+ reason: "Trivial threading.local initializer — retained in the graph but excluded from priority ranking."
2834
+ });
2835
+ }
2836
+ }
2837
+ }
2838
+ return out;
2839
+ }
2840
+ function pythonModuleImportAliases(root) {
2841
+ const importCounts = new Map();
2842
+ const importLocals = new Set();
2843
+ for (const statement of namedChildren(root)) {
2844
+ if (statement.type !== "import_statement" && statement.type !== "import_from_statement")
2845
+ continue;
2846
+ for (const binding of extractImports(statement, "python")) {
2847
+ importCounts.set(binding.local, (importCounts.get(binding.local) ?? 0) + 1);
2848
+ importLocals.add(binding.local);
2849
+ }
2850
+ }
2851
+ const writeCounts = new Map();
2852
+ const directAliases = new Map();
2853
+ for (const type of ["assignment", "annotated_assignment", "augmented_assignment", "delete_statement"]) {
2854
+ for (const mutation of root.descendantsOfType(type)) {
2855
+ if (!mutation)
2856
+ continue;
2857
+ let parent = mutation.parent;
2858
+ let moduleScoped = false;
2859
+ while (parent) {
2860
+ if (parent.type === "function_definition" || parent.type === "class_definition" || parent.type === "lambda")
2861
+ break;
2862
+ if (parent.type === "module") {
2863
+ moduleScoped = true;
2864
+ break;
2865
+ }
2866
+ parent = parent.parent;
2867
+ }
2868
+ if (!moduleScoped)
2869
+ continue;
2870
+ const targets = mutation.type === "delete_statement"
2871
+ ? namedChildren(mutation).flatMap((child) => pythonBindingTargetNames(child))
2872
+ : pythonBindingTargetNames(mutation.childForFieldName("left"));
2873
+ for (const target of targets)
2874
+ writeCounts.set(target, (writeCounts.get(target) ?? 0) + 1);
2875
+ if (!pythonDirectModuleExecution(mutation) || mutation.type !== "assignment")
2876
+ continue;
2877
+ const left = mutation.childForFieldName("left");
2878
+ const right = mutation.childForFieldName("right");
2879
+ if (left?.type === "identifier" && right?.type === "identifier")
2880
+ directAliases.set(left.text, right.text);
2881
+ }
2882
+ }
2883
+ const out = new Map();
2884
+ for (const local of importLocals) {
2885
+ if (importCounts.get(local) === 1 && (writeCounts.get(local) ?? 0) === 0)
2886
+ out.set(local, local);
2887
+ }
2888
+ for (const [alias, importedLocal] of directAliases) {
2889
+ if ((writeCounts.get(alias) ?? 0) === 1 && out.get(importedLocal) === importedLocal)
2890
+ out.set(alias, importedLocal);
2891
+ }
2892
+ return out;
2893
+ }
2894
+ function extractPythonThinDelegates(root) {
2895
+ const out = [];
2896
+ const imports = pythonModuleImportAliases(root);
2897
+ for (const fn of root.descendantsOfType("function_definition")) {
2898
+ if (!fn || hasPythonFunctionAncestor(fn))
2899
+ continue;
2900
+ const symbol = pythonQualifiedFunctionName(fn);
2901
+ const params = pythonParameterNames(fn);
2902
+ const body = fn.childForFieldName("body");
2903
+ if (!symbol || !params || !body)
2904
+ continue;
2905
+ const statements = blockStatements(body);
2906
+ if (statements.length > 0 && pythonDocstringStatement(statements[0]))
2907
+ statements.shift();
2908
+ if (statements.length !== 1 || statements[0].type !== "return_statement")
2909
+ continue;
2910
+ const expression = namedChildren(statements[0])[0];
2911
+ if (expression?.type !== "call")
2912
+ continue;
2913
+ const fnNode = expression.childForFieldName("function");
2914
+ if (fnNode?.type !== "attribute")
2915
+ continue;
2916
+ const receiver = fnNode.childForFieldName("object");
2917
+ if (receiver?.type !== "identifier" || localBindings(fn, "python").has(receiver.text))
2918
+ continue;
2919
+ const importLocal = imports.get(receiver.text);
2920
+ if (!importLocal)
2921
+ continue;
2922
+ const args = pythonCallArguments(expression);
2923
+ if (args.length !== params.length || args.some((arg, index) => arg.type !== "identifier" || arg.text !== params[index]))
2924
+ continue;
2925
+ out.push({ symbol, importLocal });
2926
+ }
2927
+ return out;
2928
+ }
2929
+ function pythonNearestFunction(node) {
2930
+ let parent = node.parent;
2931
+ while (parent) {
2932
+ if (parent.type === "function_definition")
2933
+ return parent;
2934
+ parent = parent.parent;
2935
+ }
2936
+ return null;
2937
+ }
2938
+ function pythonDirectFunctionStatement(call, owner) {
2939
+ // A direct call may be awaited and assigned before it becomes a function-body
2940
+ // statement. Unwrap only the exact RHS, never a call nested in an argument,
2941
+ // condition, comprehension, or another callable's result.
2942
+ let expression = call;
2943
+ if (expression.parent?.type === "await")
2944
+ expression = expression.parent;
2945
+ if (expression.parent?.type === "assignment") {
2946
+ const assignment = expression.parent;
2947
+ const right = assignment.childForFieldName("right");
2948
+ const left = assignment.childForFieldName("left");
2949
+ if (left?.type !== "identifier" || right?.startIndex !== expression.startIndex || right?.endIndex !== expression.endIndex)
2950
+ return false;
2951
+ expression = assignment;
2952
+ }
2953
+ const statement = expression.parent;
2954
+ if (!statement || (statement.type !== "expression_statement" && statement.type !== "return_statement"))
2955
+ return false;
2956
+ const body = statement.parent;
2957
+ const parent = body?.parent;
2958
+ // web-tree-sitter may return distinct JS wrappers for the same native node, so
2959
+ // object identity is not a valid ancestry test. Compare the exact AST span.
2960
+ return body?.type === "block"
2961
+ && parent?.type === owner.type
2962
+ && parent.startIndex === owner.startIndex
2963
+ && parent.endIndex === owner.endIndex;
2964
+ }
2965
+ function pythonConstructorChain(call) {
2966
+ const fn = call.childForFieldName("function");
2967
+ if (fn?.type !== "attribute")
2968
+ return null;
2969
+ let receiver = fn.childForFieldName("object");
2970
+ let attributes = 0;
2971
+ while (receiver?.type === "attribute") {
2972
+ attributes++;
2973
+ receiver = receiver.childForFieldName("object");
2974
+ }
2975
+ // At least one persistence/object field between constructor and terminal.
2976
+ if (attributes < 1 || receiver?.type !== "call")
2977
+ return null;
2978
+ const constructorFn = receiver.childForFieldName("function");
2979
+ if (constructorFn?.type !== "identifier")
2980
+ return null;
2981
+ const terminalArgs = call.childForFieldName("arguments");
2982
+ const constructorArgs = receiver.childForFieldName("arguments");
2983
+ if (terminalArgs?.descendantsOfType("call").some(Boolean) || constructorArgs?.descendantsOfType("call").some(Boolean))
2984
+ return null;
2985
+ return { constructorLocal: constructorFn.text, callee: fn.text };
2986
+ }
2987
+ function extractPythonConstructorChainCalls(root) {
2988
+ const out = [];
2989
+ for (const call of root.descendantsOfType("call")) {
2990
+ if (!call)
2991
+ continue;
2992
+ const owner = pythonNearestFunction(call);
2993
+ if (!owner || hasPythonFunctionAncestor(owner) || !pythonDirectFunctionStatement(call, owner))
2994
+ continue;
2995
+ const shape = pythonConstructorChain(call);
2996
+ const caller = pythonQualifiedFunctionName(owner);
2997
+ if (shape && caller)
2998
+ out.push({ caller, ...shape });
2999
+ }
3000
+ return out;
3001
+ }
3002
+ function extractPythonModuleAttributes(root, imports) {
3003
+ const counts = new Map();
3004
+ for (const binding of imports)
3005
+ counts.set(binding.local, (counts.get(binding.local) ?? 0) + 1);
3006
+ const modules = new Set(imports.filter((binding) => binding.kind === "module" && counts.get(binding.local) === 1).map((binding) => binding.local));
3007
+ const globalWrites = new Set(root.descendantsOfType("assignment")
3008
+ .filter((assignment) => assignment?.parent?.type === "module")
3009
+ .map((assignment) => assignment.childForFieldName("left")?.text).filter((name) => Boolean(name)));
3010
+ const boundLocals = new Map();
3011
+ const out = [];
3012
+ for (const attr of root.descendantsOfType("attribute")) {
3013
+ if (!attr || attr.parent?.type === "call" && attr.parent.childForFieldName("function")?.startIndex === attr.startIndex && attr.parent.childForFieldName("function")?.endIndex === attr.endIndex)
3014
+ continue;
3015
+ if (attr.parent?.type === "attribute" && attr.parent.childForFieldName("object") === attr)
3016
+ continue;
3017
+ const object = attr.childForFieldName("object");
3018
+ const name = attr.childForFieldName("attribute");
3019
+ if (object?.type !== "identifier" || !name || !modules.has(object.text))
3020
+ continue;
3021
+ const fn = pythonNearestFunction(attr);
3022
+ if (fn) {
3023
+ let bindings = boundLocals.get(fn.startIndex);
3024
+ if (!bindings)
3025
+ boundLocals.set(fn.startIndex, (bindings = localBindings(fn, "python")));
3026
+ if (bindings.has(object.text))
3027
+ continue;
3028
+ }
3029
+ // Never count a reassigned global alias or a module-level class/function with
3030
+ // the same binding. Ambiguity fails closed rather than guessing an origin.
3031
+ if (globalWrites.has(object.text))
3032
+ continue;
3033
+ out.push({ local: object.text, attribute: name.text });
3034
+ }
3035
+ return out;
3036
+ }
1906
3037
  export function extractTreeSitterStructure(content, language, relPath = "") {
1907
3038
  const parser = parserFor(language);
1908
3039
  if (!parser)
@@ -1966,6 +3097,7 @@ export function extractTreeSitterStructure(content, language, relPath = "") {
1966
3097
  const pythonDependencies = language === "python"
1967
3098
  ? extractPythonDependencies(root, imports, new Set((pythonBehaviorContracts ?? []).map((contract) => contract.handler).filter((handler) => Boolean(handler))))
1968
3099
  : undefined;
3100
+ const pythonMockScopes = language === "python" ? pythonScopedMockMethods(root, imports) : undefined;
1969
3101
  const result = {
1970
3102
  ...(language === "java" ? { packageName: javaPackage(root), javaClasses: javaClassInfos(root), javaProofCalls: extractJavaProofCalls(root, imports) } : {}),
1971
3103
  ...(language === "go" ? { packageName: goPackage(root), goCtorResults: extractGoCtorResults(root) } : {}),
@@ -1975,7 +3107,19 @@ export function extractTreeSitterStructure(content, language, relPath = "") {
1975
3107
  imports,
1976
3108
  calls,
1977
3109
  ...(language === "go" ? { goProofCalls: extractGoProofCalls(root, imports) } : {}),
1978
- ...(language === "python" ? { pythonProofCalls: extractPythonProofCalls(root), pythonBehaviorContracts, pythonDependencies } : {})
3110
+ ...(language === "python" ? {
3111
+ pythonProofCalls: extractPythonProofCalls(root, imports),
3112
+ pythonMockedMethods: pythonMockedMethods(root, imports),
3113
+ pythonMocksByTest: pythonMockScopes.byTest,
3114
+ pythonUnknownMockScopeTests: pythonMockScopes.unknown,
3115
+ pythonClasses: pythonClassInfos(root),
3116
+ pythonBehaviorContracts,
3117
+ pythonDependencies,
3118
+ pythonRankingExclusions: extractPythonDeclarationRankingExclusions(root),
3119
+ pythonThinDelegates: extractPythonThinDelegates(root),
3120
+ pythonModuleAttributes: extractPythonModuleAttributes(root, imports),
3121
+ pythonConstructorChainCalls: extractPythonConstructorChainCalls(root)
3122
+ } : {})
1979
3123
  };
1980
3124
  tree.delete();
1981
3125
  return result;