@orangepro/orangepro-mcp 0.2.44 → 0.2.46

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))
@@ -1935,131 +1947,665 @@ function pythonBindingNames(node) {
1935
1947
  }
1936
1948
  return [];
1937
1949
  }
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 ?? ""))
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)
1945
2385
  return [];
1946
2386
  const lexicalNodes = pythonLexicalNodes(block);
1947
- const assignments = lexicalNodes.filter((node) => node.type === "assignment" || node.type === "annotated_assignment");
2387
+ const assignments = lexicalNodes
2388
+ .filter((node) => node.type === "assignment" || node.type === "annotated_assignment")
2389
+ .map(pythonDirectAssignment)
2390
+ .filter((v) => Boolean(v));
1948
2391
  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);
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);
1955
2399
  }
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)
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")
1960
2408
  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))
2409
+ const classRef = pythonClassRefFromConstructor(value);
2410
+ if (!classRef || (writes.get(target.text) ?? 0) !== 1 || receivers.has(target.text))
1964
2411
  continue;
1965
- if ((mutationCounts.get(parts.callee) ?? 0) > 0)
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")
1966
2416
  continue;
1967
- instances.set(target.text, { className: parts.callee, assignedAt: target.startIndex });
2417
+ if ([...receivers.values()].some((receiver) => receiver.classRef === target.text))
2418
+ return [];
1968
2419
  }
1969
- for (const { target, value } of orderedAssignments) {
1970
- if (target.type === "identifier" && value.type === "identifier" && instances.has(value.text))
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)))
1971
2449
  return [];
1972
2450
  }
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)))
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)))
1990
2453
  return [];
1991
2454
  }
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);
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}`);
1997
2475
  });
1998
- if (instanceCalls.length !== 1)
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)
1999
2516
  return [];
2000
- for (const { target, value } of orderedAssignments) {
2001
- if (target.type !== "identifier" || mutationCounts.get(target.text) !== 1)
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)
2002
2521
  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
- }
2522
+ const call = pythonExactCall(value);
2523
+ if (call?.id === target.node.id)
2524
+ results.set(resultName.text, target);
2010
2525
  }
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 });
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" }];
2029
2529
  }
2030
- return out;
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
+ : [];
2031
2535
  }
2032
- function extractPythonProofCalls(root) {
2536
+ function extractPythonProofCalls(root, imports) {
2033
2537
  const out = [];
2034
2538
  const seen = new Set();
2035
- const add = (testName, shadowed, call, instanceClass) => {
2539
+ const fixtures = pythonFixtureBindings(root);
2540
+ const add = (testName, shadowed, call, instanceClass, associationRule) => {
2036
2541
  if (!call)
2037
2542
  return;
2038
2543
  const key = `${testName}|${instanceClass ?? ""}|${call.qualifier ?? ""}|${call.callee}`;
2039
2544
  if (seen.has(key))
2040
2545
  return;
2041
2546
  seen.add(key);
2042
- out.push({ caller: testName, testName, ...call, shadowed: [...shadowed], assertion: "pytest_assert", ...(instanceClass ? { instanceClass } : {}) });
2547
+ out.push({
2548
+ caller: testName,
2549
+ testName,
2550
+ ...call,
2551
+ shadowed: [...shadowed],
2552
+ assertion: "pytest_assert",
2553
+ ...(instanceClass ? { instanceClass } : {}),
2554
+ ...(associationRule ? { associationRule } : {})
2555
+ });
2043
2556
  };
2044
- const processBlock = (block, testName, shadowed) => {
2045
- for (const associated of pythonAssignedInstanceProofCalls(block)) {
2046
- add(testName, shadowed, associated.call, associated.instanceClass);
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);
2047
2575
  }
2048
2576
  for (const stmt of blockStatements(block)) {
2049
- if (stmt.type === "assert_statement")
2050
- 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
+ }
2051
2594
  if (stmt.type === "function_definition" || stmt.type === "class_definition" || stmt.type === "lambda")
2052
2595
  continue;
2053
2596
  for (const child of namedChildren(stmt)) {
2054
2597
  if (child.type === "block")
2055
- processBlock(child, testName, shadowed);
2598
+ processBlock(child, testFunction, testName, shadowed, setup);
2056
2599
  }
2057
2600
  }
2058
2601
  };
2059
2602
  const walk = (node, insideFunction, testClass) => {
2060
2603
  if (!insideFunction && node.type === "class_definition") {
2061
2604
  const name = node.childForFieldName("name")?.text;
2062
- 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;
2063
2609
  for (const child of namedChildren(node))
2064
2610
  walk(child, false, nextTestClass);
2065
2611
  return;
@@ -2068,7 +2614,7 @@ function extractPythonProofCalls(root) {
2068
2614
  const name = functionName(node, "python");
2069
2615
  const body = node.childForFieldName("body");
2070
2616
  if (name && /^test_[A-Za-z0-9_]*$/.test(name) && body) {
2071
- 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));
2072
2618
  }
2073
2619
  if (insideFunction)
2074
2620
  return;
@@ -2079,6 +2625,483 @@ function extractPythonProofCalls(root) {
2079
2625
  walk(root, false, null);
2080
2626
  return out;
2081
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 modules = typeof module === "string" ? [module] : module;
2691
+ const dotted = pythonDottedName(base);
2692
+ if (!dotted)
2693
+ return false;
2694
+ const parts = dotted.split(".");
2695
+ if (parts.length === 1) {
2696
+ const binding = bindings.get(parts[0]);
2697
+ return Boolean(binding?.kind === "named" && modules.includes(binding.module) && binding.imported && allowed.has(binding.imported));
2698
+ }
2699
+ if (parts.length === 2 && allowed.has(parts[1])) {
2700
+ const binding = bindings.get(parts[0]);
2701
+ return Boolean(binding?.kind === "module" && modules.includes(binding.module));
2702
+ }
2703
+ return false;
2704
+ }
2705
+ function pythonClassHasMethod(node) {
2706
+ const body = node.childForFieldName("body");
2707
+ if (!body)
2708
+ return false;
2709
+ const visit = (current) => {
2710
+ if (current !== body && current.type === "class_definition")
2711
+ return false;
2712
+ if (current.type === "function_definition")
2713
+ return true;
2714
+ return namedChildren(current).some(visit);
2715
+ };
2716
+ return namedChildren(body).some(visit);
2717
+ }
2718
+ function pythonParameterNames(fn) {
2719
+ const params = fn.childForFieldName("parameters");
2720
+ if (!params)
2721
+ return null;
2722
+ const out = [];
2723
+ for (const parameter of namedChildren(params)) {
2724
+ if (parameter.type === "identifier") {
2725
+ out.push(parameter.text);
2726
+ continue;
2727
+ }
2728
+ if (parameter.type === "default_parameter" || parameter.type === "typed_parameter" || parameter.type === "typed_default_parameter") {
2729
+ const name = parameter.childForFieldName("name") ?? namedChildren(parameter)[0] ?? null;
2730
+ if (name?.type !== "identifier")
2731
+ return null;
2732
+ out.push(name.text);
2733
+ continue;
2734
+ }
2735
+ // Splat/pattern/separator shapes are not bare forwarding parameters.
2736
+ return null;
2737
+ }
2738
+ return out;
2739
+ }
2740
+ function pythonLiteralOrParameter(node, params) {
2741
+ if (!node)
2742
+ return false;
2743
+ if (node.type === "identifier")
2744
+ return params.has(node.text);
2745
+ // An f-string with {interpolation} computes a value; only plain literals qualify.
2746
+ if ((node.type === "string" || node.type === "concatenated_string") && node.descendantsOfType("interpolation").length > 0)
2747
+ return false;
2748
+ return new Set(["none", "true", "false", "integer", "float", "string", "concatenated_string"]).has(node.type);
2749
+ }
2750
+ function pythonDocstringStatement(statement) {
2751
+ if (statement.type !== "expression_statement")
2752
+ return false;
2753
+ const expression = namedChildren(statement)[0];
2754
+ return expression?.type === "string" || expression?.type === "concatenated_string";
2755
+ }
2756
+ function pythonSuperInitStatement(statement, params = new Set()) {
2757
+ if (statement.type !== "expression_statement")
2758
+ return false;
2759
+ const call = namedChildren(statement)[0];
2760
+ if (call?.type !== "call")
2761
+ return false;
2762
+ // super().__init__(...) may only FORWARD this constructor's own parameters or
2763
+ // literals (positionally or by keyword). Any computed argument is behavior.
2764
+ const forwardsOnly = pythonCallArguments(call).every((arg) => arg.type === "keyword_argument"
2765
+ ? pythonLiteralOrParameter(arg.childForFieldName("value"), params)
2766
+ : pythonLiteralOrParameter(arg, params));
2767
+ if (!forwardsOnly)
2768
+ return false;
2769
+ const fn = call.childForFieldName("function");
2770
+ if (fn?.type !== "attribute" || fn.childForFieldName("attribute")?.text !== "__init__")
2771
+ return false;
2772
+ const receiver = fn.childForFieldName("object");
2773
+ if (receiver?.type !== "call" || pythonCallArguments(receiver).length !== 0)
2774
+ return false;
2775
+ return receiver.childForFieldName("function")?.type === "identifier" && receiver.childForFieldName("function")?.text === "super";
2776
+ }
2777
+ function pythonTrivialSelfAssignment(statement, params) {
2778
+ if (statement.type !== "expression_statement")
2779
+ return false;
2780
+ const assignment = namedChildren(statement)[0];
2781
+ if (assignment?.type !== "assignment")
2782
+ return false;
2783
+ const left = assignment.childForFieldName("left");
2784
+ if (left?.type !== "attribute" || left.childForFieldName("object")?.type !== "identifier" || left.childForFieldName("object")?.text !== "self")
2785
+ return false;
2786
+ return pythonLiteralOrParameter(assignment.childForFieldName("right"), params);
2787
+ }
2788
+ function pythonTrivialInitializer(classNode) {
2789
+ const body = classNode.childForFieldName("body");
2790
+ if (!body)
2791
+ return null;
2792
+ const methods = namedChildren(body).flatMap((statement) => {
2793
+ if (statement.type === "function_definition")
2794
+ return [statement];
2795
+ if (statement.type === "decorated_definition") {
2796
+ const fn = namedChildren(statement).find((child) => child.type === "function_definition");
2797
+ return fn ? [fn] : [];
2798
+ }
2799
+ return [];
2800
+ }).filter((fn) => fn.childForFieldName("name")?.text === "__init__");
2801
+ if (methods.length !== 1)
2802
+ return null;
2803
+ const fn = methods[0];
2804
+ const paramsList = pythonParameterNames(fn);
2805
+ const fnBody = fn.childForFieldName("body");
2806
+ if (!paramsList || !fnBody)
2807
+ return null;
2808
+ const params = new Set(paramsList);
2809
+ const statements = blockStatements(fnBody);
2810
+ for (let i = 0; i < statements.length; i++) {
2811
+ const statement = statements[i];
2812
+ if (i === 0 && pythonDocstringStatement(statement))
2813
+ continue;
2814
+ if (statement.type === "pass_statement" || pythonSuperInitStatement(statement, params) || pythonTrivialSelfAssignment(statement, params))
2815
+ continue;
2816
+ return null;
2817
+ }
2818
+ return pythonQualifiedFunctionName(fn) ?? null;
2819
+ }
2820
+ const PYTHON_TYPING_SHAPES = new Set(["TypedDict", "NamedTuple", "Protocol"]);
2821
+ const PYTHON_TYPING_MODULES = ["typing", "typing_extensions"];
2822
+ const PYTHON_PYDANTIC_SHAPES = new Set(["BaseModel"]);
2823
+ const PYTHON_PYDANTIC_MODULES = ["pydantic", "pydantic.main", "pydantic.v1"];
2824
+ const PYTHON_DATACLASS_MODULES = ["dataclasses", "pydantic.dataclasses"];
2825
+ /** `@dataclass`, `@dataclass(...)`, `@dataclasses.dataclass(...)` (stdlib or pydantic's drop-in). */
2826
+ function pythonIsStdlibDataclassDecorator(decorator, bindings) {
2827
+ let expression = namedChildren(decorator)[0] ?? null;
2828
+ if (expression?.type === "call")
2829
+ expression = expression.childForFieldName("function");
2830
+ return Boolean(expression && pythonBaseIsStdlib(expression, bindings, PYTHON_DATACLASS_MODULES, new Set(["dataclass"])));
2831
+ }
2832
+ /** Class body holds only field declarations: annotations (with or without a default),
2833
+ * plain attribute assignments, a docstring, or `pass`. No methods, no nested classes,
2834
+ * no statements that run logic. */
2835
+ function pythonFieldOnlyBody(classNode) {
2836
+ const body = classNode.childForFieldName("body");
2837
+ if (!body)
2838
+ return false;
2839
+ const statements = blockStatements(body);
2840
+ if (statements.length === 0)
2841
+ return false;
2842
+ return statements.every((statement, index) => {
2843
+ if (statement.type === "pass_statement")
2844
+ return true;
2845
+ if (index === 0 && pythonDocstringStatement(statement))
2846
+ return true;
2847
+ if (statement.type !== "expression_statement")
2848
+ return false;
2849
+ const inner = namedChildren(statement);
2850
+ // A lambda in a field value (e.g. custom JSON encoders) is behavior, not data.
2851
+ return inner.length === 1 && inner[0].type === "assignment" && inner[0].childForFieldName("left")?.type === "identifier"
2852
+ && inner[0].descendantsOfType("lambda").length === 0;
2853
+ });
2854
+ }
2855
+ function extractPythonDeclarationRankingExclusions(root) {
2856
+ const out = [];
2857
+ for (const classNode of root.descendantsOfType("class_definition")) {
2858
+ if (!classNode || hasPythonFunctionAncestor(classNode))
2859
+ continue;
2860
+ // Module-level classes only (decorated or not): nested-class symbol names are
2861
+ // not guaranteed to line up with the analyzer's symbol table.
2862
+ const decorated = classNode.parent?.type === "decorated_definition" ? classNode.parent : null;
2863
+ if ((decorated ?? classNode).parent?.type !== "module")
2864
+ continue;
2865
+ const name = classNode.childForFieldName("name")?.text;
2866
+ if (!name)
2867
+ continue;
2868
+ const bases = classNode.childForFieldName("superclasses");
2869
+ const bindings = pythonImportsBefore(root, (decorated ?? classNode).startIndex);
2870
+ const baseNodes = bases ? namedChildren(bases) : [];
2871
+ const hasMethod = pythonClassHasMethod(classNode);
2872
+ if (baseNodes.some((base) => pythonBaseIsStdlib(base, bindings, "enum", PYTHON_ENUM_TYPES)) && !hasMethod) {
2873
+ out.push({
2874
+ symbol: name,
2875
+ code: "python_stdlib_enum_declaration",
2876
+ reason: "Standard-library enum declaration without methods — retained in the graph but excluded from priority ranking."
2877
+ });
2878
+ continue;
2879
+ }
2880
+ // Data-shape declarations: a TypedDict/NamedTuple/Protocol, a pydantic model, or a
2881
+ // stdlib @dataclass whose body only declares fields. No method ⇒ no behavior to test.
2882
+ if (!hasMethod && pythonFieldOnlyBody(classNode)) {
2883
+ const typedShape = baseNodes.some((base) => pythonBaseIsStdlib(base, bindings, PYTHON_TYPING_MODULES, PYTHON_TYPING_SHAPES) ||
2884
+ pythonBaseIsStdlib(base, bindings, PYTHON_PYDANTIC_MODULES, PYTHON_PYDANTIC_SHAPES));
2885
+ const dataclassShape = Boolean(decorated) && namedChildren(decorated).some((child) => child.type === "decorator" && pythonIsStdlibDataclassDecorator(child, bindings));
2886
+ if (typedShape || dataclassShape) {
2887
+ out.push({
2888
+ symbol: name,
2889
+ code: "python_data_shape_declaration",
2890
+ reason: "Data-shape declaration (fields only, no methods) — retained in the graph but excluded from priority ranking."
2891
+ });
2892
+ continue;
2893
+ }
2894
+ }
2895
+ // Trivial constructor on ANY class: the body only stores parameters/literals on
2896
+ // self, forwards parameters/literals to super().__init__, or passes.
2897
+ const initializer = pythonTrivialInitializer(classNode);
2898
+ if (initializer) {
2899
+ out.push({
2900
+ symbol: initializer,
2901
+ code: "python_trivial_constructor",
2902
+ reason: "Trivial constructor (stores or forwards its parameters only) — retained in the graph but excluded from priority ranking."
2903
+ });
2904
+ }
2905
+ }
2906
+ return out;
2907
+ }
2908
+ function pythonModuleImportAliases(root) {
2909
+ const importCounts = new Map();
2910
+ const importLocals = new Set();
2911
+ for (const statement of namedChildren(root)) {
2912
+ if (statement.type !== "import_statement" && statement.type !== "import_from_statement")
2913
+ continue;
2914
+ for (const binding of extractImports(statement, "python")) {
2915
+ importCounts.set(binding.local, (importCounts.get(binding.local) ?? 0) + 1);
2916
+ importLocals.add(binding.local);
2917
+ }
2918
+ }
2919
+ const writeCounts = new Map();
2920
+ const directAliases = new Map();
2921
+ for (const type of ["assignment", "annotated_assignment", "augmented_assignment", "delete_statement"]) {
2922
+ for (const mutation of root.descendantsOfType(type)) {
2923
+ if (!mutation)
2924
+ continue;
2925
+ let parent = mutation.parent;
2926
+ let moduleScoped = false;
2927
+ while (parent) {
2928
+ if (parent.type === "function_definition" || parent.type === "class_definition" || parent.type === "lambda")
2929
+ break;
2930
+ if (parent.type === "module") {
2931
+ moduleScoped = true;
2932
+ break;
2933
+ }
2934
+ parent = parent.parent;
2935
+ }
2936
+ if (!moduleScoped)
2937
+ continue;
2938
+ const targets = mutation.type === "delete_statement"
2939
+ ? namedChildren(mutation).flatMap((child) => pythonBindingTargetNames(child))
2940
+ : pythonBindingTargetNames(mutation.childForFieldName("left"));
2941
+ for (const target of targets)
2942
+ writeCounts.set(target, (writeCounts.get(target) ?? 0) + 1);
2943
+ if (!pythonDirectModuleExecution(mutation) || mutation.type !== "assignment")
2944
+ continue;
2945
+ const left = mutation.childForFieldName("left");
2946
+ const right = mutation.childForFieldName("right");
2947
+ if (left?.type === "identifier" && right?.type === "identifier")
2948
+ directAliases.set(left.text, right.text);
2949
+ }
2950
+ }
2951
+ const out = new Map();
2952
+ for (const local of importLocals) {
2953
+ if (importCounts.get(local) === 1 && (writeCounts.get(local) ?? 0) === 0)
2954
+ out.set(local, local);
2955
+ }
2956
+ for (const [alias, importedLocal] of directAliases) {
2957
+ if ((writeCounts.get(alias) ?? 0) === 1 && out.get(importedLocal) === importedLocal)
2958
+ out.set(alias, importedLocal);
2959
+ }
2960
+ return out;
2961
+ }
2962
+ function extractPythonThinDelegates(root) {
2963
+ const out = [];
2964
+ const imports = pythonModuleImportAliases(root);
2965
+ for (const fn of root.descendantsOfType("function_definition")) {
2966
+ if (!fn || hasPythonFunctionAncestor(fn))
2967
+ continue;
2968
+ const symbol = pythonQualifiedFunctionName(fn);
2969
+ const params = pythonParameterNames(fn);
2970
+ const body = fn.childForFieldName("body");
2971
+ if (!symbol || !params || !body)
2972
+ continue;
2973
+ const statements = blockStatements(body);
2974
+ if (statements.length > 0 && pythonDocstringStatement(statements[0]))
2975
+ statements.shift();
2976
+ if (statements.length !== 1 || statements[0].type !== "return_statement")
2977
+ continue;
2978
+ const expression = namedChildren(statements[0])[0];
2979
+ if (expression?.type !== "call")
2980
+ continue;
2981
+ const fnNode = expression.childForFieldName("function");
2982
+ if (fnNode?.type !== "attribute")
2983
+ continue;
2984
+ const receiver = fnNode.childForFieldName("object");
2985
+ if (receiver?.type !== "identifier" || localBindings(fn, "python").has(receiver.text))
2986
+ continue;
2987
+ const importLocal = imports.get(receiver.text);
2988
+ if (!importLocal)
2989
+ continue;
2990
+ const args = pythonCallArguments(expression);
2991
+ if (args.length !== params.length || args.some((arg, index) => arg.type !== "identifier" || arg.text !== params[index]))
2992
+ continue;
2993
+ out.push({ symbol, importLocal });
2994
+ }
2995
+ return out;
2996
+ }
2997
+ function pythonNearestFunction(node) {
2998
+ let parent = node.parent;
2999
+ while (parent) {
3000
+ if (parent.type === "function_definition")
3001
+ return parent;
3002
+ parent = parent.parent;
3003
+ }
3004
+ return null;
3005
+ }
3006
+ function pythonDirectFunctionStatement(call, owner) {
3007
+ // A direct call may be awaited and assigned before it becomes a function-body
3008
+ // statement. Unwrap only the exact RHS, never a call nested in an argument,
3009
+ // condition, comprehension, or another callable's result.
3010
+ let expression = call;
3011
+ if (expression.parent?.type === "await")
3012
+ expression = expression.parent;
3013
+ if (expression.parent?.type === "assignment") {
3014
+ const assignment = expression.parent;
3015
+ const right = assignment.childForFieldName("right");
3016
+ const left = assignment.childForFieldName("left");
3017
+ if (left?.type !== "identifier" || right?.startIndex !== expression.startIndex || right?.endIndex !== expression.endIndex)
3018
+ return false;
3019
+ expression = assignment;
3020
+ }
3021
+ const statement = expression.parent;
3022
+ if (!statement || (statement.type !== "expression_statement" && statement.type !== "return_statement"))
3023
+ return false;
3024
+ const body = statement.parent;
3025
+ const parent = body?.parent;
3026
+ // web-tree-sitter may return distinct JS wrappers for the same native node, so
3027
+ // object identity is not a valid ancestry test. Compare the exact AST span.
3028
+ return body?.type === "block"
3029
+ && parent?.type === owner.type
3030
+ && parent.startIndex === owner.startIndex
3031
+ && parent.endIndex === owner.endIndex;
3032
+ }
3033
+ function pythonConstructorChain(call) {
3034
+ const fn = call.childForFieldName("function");
3035
+ if (fn?.type !== "attribute")
3036
+ return null;
3037
+ let receiver = fn.childForFieldName("object");
3038
+ let attributes = 0;
3039
+ while (receiver?.type === "attribute") {
3040
+ attributes++;
3041
+ receiver = receiver.childForFieldName("object");
3042
+ }
3043
+ // At least one persistence/object field between constructor and terminal.
3044
+ if (attributes < 1 || receiver?.type !== "call")
3045
+ return null;
3046
+ const constructorFn = receiver.childForFieldName("function");
3047
+ if (constructorFn?.type !== "identifier")
3048
+ return null;
3049
+ const terminalArgs = call.childForFieldName("arguments");
3050
+ const constructorArgs = receiver.childForFieldName("arguments");
3051
+ if (terminalArgs?.descendantsOfType("call").some(Boolean) || constructorArgs?.descendantsOfType("call").some(Boolean))
3052
+ return null;
3053
+ return { constructorLocal: constructorFn.text, callee: fn.text };
3054
+ }
3055
+ function extractPythonConstructorChainCalls(root) {
3056
+ const out = [];
3057
+ for (const call of root.descendantsOfType("call")) {
3058
+ if (!call)
3059
+ continue;
3060
+ const owner = pythonNearestFunction(call);
3061
+ if (!owner || hasPythonFunctionAncestor(owner) || !pythonDirectFunctionStatement(call, owner))
3062
+ continue;
3063
+ const shape = pythonConstructorChain(call);
3064
+ const caller = pythonQualifiedFunctionName(owner);
3065
+ if (shape && caller)
3066
+ out.push({ caller, ...shape });
3067
+ }
3068
+ return out;
3069
+ }
3070
+ function extractPythonModuleAttributes(root, imports) {
3071
+ const counts = new Map();
3072
+ for (const binding of imports)
3073
+ counts.set(binding.local, (counts.get(binding.local) ?? 0) + 1);
3074
+ const modules = new Set(imports.filter((binding) => binding.kind === "module" && counts.get(binding.local) === 1).map((binding) => binding.local));
3075
+ const globalWrites = new Set(root.descendantsOfType("assignment")
3076
+ .filter((assignment) => assignment?.parent?.type === "module")
3077
+ .map((assignment) => assignment.childForFieldName("left")?.text).filter((name) => Boolean(name)));
3078
+ const boundLocals = new Map();
3079
+ const out = [];
3080
+ for (const attr of root.descendantsOfType("attribute")) {
3081
+ if (!attr || attr.parent?.type === "call" && attr.parent.childForFieldName("function")?.startIndex === attr.startIndex && attr.parent.childForFieldName("function")?.endIndex === attr.endIndex)
3082
+ continue;
3083
+ if (attr.parent?.type === "attribute" && attr.parent.childForFieldName("object") === attr)
3084
+ continue;
3085
+ const object = attr.childForFieldName("object");
3086
+ const name = attr.childForFieldName("attribute");
3087
+ if (object?.type !== "identifier" || !name || !modules.has(object.text))
3088
+ continue;
3089
+ const fn = pythonNearestFunction(attr);
3090
+ if (fn) {
3091
+ let bindings = boundLocals.get(fn.startIndex);
3092
+ if (!bindings)
3093
+ boundLocals.set(fn.startIndex, (bindings = localBindings(fn, "python")));
3094
+ if (bindings.has(object.text))
3095
+ continue;
3096
+ }
3097
+ // Never count a reassigned global alias or a module-level class/function with
3098
+ // the same binding. Ambiguity fails closed rather than guessing an origin.
3099
+ if (globalWrites.has(object.text))
3100
+ continue;
3101
+ out.push({ local: object.text, attribute: name.text });
3102
+ }
3103
+ return out;
3104
+ }
2082
3105
  export function extractTreeSitterStructure(content, language, relPath = "") {
2083
3106
  const parser = parserFor(language);
2084
3107
  if (!parser)
@@ -2142,6 +3165,7 @@ export function extractTreeSitterStructure(content, language, relPath = "") {
2142
3165
  const pythonDependencies = language === "python"
2143
3166
  ? extractPythonDependencies(root, imports, new Set((pythonBehaviorContracts ?? []).map((contract) => contract.handler).filter((handler) => Boolean(handler))))
2144
3167
  : undefined;
3168
+ const pythonMockScopes = language === "python" ? pythonScopedMockMethods(root, imports) : undefined;
2145
3169
  const result = {
2146
3170
  ...(language === "java" ? { packageName: javaPackage(root), javaClasses: javaClassInfos(root), javaProofCalls: extractJavaProofCalls(root, imports) } : {}),
2147
3171
  ...(language === "go" ? { packageName: goPackage(root), goCtorResults: extractGoCtorResults(root) } : {}),
@@ -2151,7 +3175,19 @@ export function extractTreeSitterStructure(content, language, relPath = "") {
2151
3175
  imports,
2152
3176
  calls,
2153
3177
  ...(language === "go" ? { goProofCalls: extractGoProofCalls(root, imports) } : {}),
2154
- ...(language === "python" ? { pythonProofCalls: extractPythonProofCalls(root), pythonBehaviorContracts, pythonDependencies } : {})
3178
+ ...(language === "python" ? {
3179
+ pythonProofCalls: extractPythonProofCalls(root, imports),
3180
+ pythonMockedMethods: pythonMockedMethods(root, imports),
3181
+ pythonMocksByTest: pythonMockScopes.byTest,
3182
+ pythonUnknownMockScopeTests: pythonMockScopes.unknown,
3183
+ pythonClasses: pythonClassInfos(root),
3184
+ pythonBehaviorContracts,
3185
+ pythonDependencies,
3186
+ pythonRankingExclusions: extractPythonDeclarationRankingExclusions(root),
3187
+ pythonThinDelegates: extractPythonThinDelegates(root),
3188
+ pythonModuleAttributes: extractPythonModuleAttributes(root, imports),
3189
+ pythonConstructorChainCalls: extractPythonConstructorChainCalls(root)
3190
+ } : {})
2155
3191
  };
2156
3192
  tree.delete();
2157
3193
  return result;