@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.
- package/dist/local/analyze/analyzer.js +322 -30
- package/dist/local/analyze/treeSitter/engine.js +1126 -90
- package/dist/local/autoProve.js +208 -102
- package/dist/local/operations.js +82 -18
- package/dist/local/proofDoctor.js +23 -3
- package/dist/local/provenance.js +2 -2
- package/dist/local/score/risk.js +339 -111
- package/dist/local/score/riskConfig.js +21 -1
- package/dist/local/viz/behaviorReportData.js +83 -20
- package/dist/local/viz/behaviorReportHtml.js +13 -2
- package/package.json +1 -1
- package/scripts/spikes/python-dynamic-proof-spike.mjs +399 -103
- package/scripts/spikes/python-mutate.py +72 -24
|
@@ -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
|
-
|
|
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
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
const
|
|
1942
|
-
if (
|
|
1943
|
-
return
|
|
1944
|
-
|
|
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
|
|
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
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
for (const
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
if (
|
|
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
|
|
1962
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2417
|
+
if ([...receivers.values()].some((receiver) => receiver.classRef === target.text))
|
|
2418
|
+
return [];
|
|
1968
2419
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
|
1974
|
-
...
|
|
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
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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 (
|
|
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
|
-
|
|
2001
|
-
|
|
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
|
|
2004
|
-
|
|
2005
|
-
|
|
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
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
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
|
|
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" ? {
|
|
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;
|