ucn 5.3.4 → 5.3.6
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/.claude/skills/ucn/SKILL.md +22 -0
- package/core/account.js +5 -1
- package/core/analysis.js +18 -0
- package/core/cache.js +5 -1
- package/core/callers.js +989 -124
- package/core/confidence.js +26 -14
- package/core/imports.js +18 -4
- package/core/index-ir.js +8 -3
- package/core/ir.js +1 -1
- package/core/output/analysis.js +20 -8
- package/core/output/provenance.js +41 -0
- package/core/output/public.js +2 -1
- package/core/output/shared.js +3 -0
- package/core/provenance-facts.js +287 -0
- package/core/provenance-overload.js +118 -0
- package/core/provenance.js +423 -0
- package/core/python-fixture-flow.js +216 -0
- package/core/receiver-types.js +26 -0
- package/core/rust-result-flow.js +206 -0
- package/core/verify.js +7 -3
- package/languages/adapter.js +2 -0
- package/languages/c-family.js +14 -1
- package/languages/csharp.js +21 -9
- package/languages/go.js +105 -86
- package/languages/java.js +23 -10
- package/languages/javascript.js +129 -18
- package/languages/python.js +202 -35
- package/languages/rust.js +185 -77
- package/languages/type-evidence.js +63 -0
- package/package.json +2 -2
package/languages/python.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* class definitions, and state objects (constants).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
9
|
+
|
|
10
|
+
|
|
8
11
|
const {
|
|
9
12
|
traverseTree,
|
|
10
13
|
traverseTreeCached,
|
|
@@ -1661,7 +1664,7 @@ function findCallsInCode(code, parser) {
|
|
|
1661
1664
|
const aliasesStack = [];
|
|
1662
1665
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
1663
1666
|
const nonCallableNamesStack = [];
|
|
1664
|
-
const localVarTypes = new
|
|
1667
|
+
const localVarTypes = new ReceiverTypeMap(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1665
1668
|
const declaredVarTypes = new Map(); // Compiler-checked annotations survive later assignments
|
|
1666
1669
|
const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
|
|
1667
1670
|
// varName -> concrete union alternatives. `None` is retained only for a
|
|
@@ -1673,7 +1676,7 @@ function findCallsInCode(code, parser) {
|
|
|
1673
1676
|
const localDictValueTypes = new Map(); // local dict -> exact string-key value types
|
|
1674
1677
|
const localSubscriptSources = new Map(); // local value -> exact typed container selected by []
|
|
1675
1678
|
const localVarStdlibContracts = new Map(); // variable -> stdlib module proving its type flow
|
|
1676
|
-
const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment
|
|
1679
|
+
const assignmentRhsReceiverTypes = new Map(); // call-node id -> pre-assignment type and origin
|
|
1677
1680
|
const constructedReceiverVars = new Set(); // exact constructor-result bindings
|
|
1678
1681
|
const withBindingVars = new Set(); // names produced by a context-manager as-target
|
|
1679
1682
|
// Member-access aliases (fix #218): `append = output.append` makes a later
|
|
@@ -1689,7 +1692,7 @@ function findCallsInCode(code, parser) {
|
|
|
1689
1692
|
// `_Segment.line()` dispatch through the imported/local class object.
|
|
1690
1693
|
// Keep this separate from callable aliases: receiver evidence must be
|
|
1691
1694
|
// scope-local, position-aware, and invalidated by every later write.
|
|
1692
|
-
const classValueAliases = new
|
|
1695
|
+
const classValueAliases = new ReceiverTypeMap();
|
|
1693
1696
|
const classValueAliasesStack = [];
|
|
1694
1697
|
const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
|
|
1695
1698
|
const moduleImportSpecifiers = new Set(); // Exact dotted imports: import rich.repr
|
|
@@ -2061,7 +2064,7 @@ function findCallsInCode(code, parser) {
|
|
|
2061
2064
|
...(generatorSendType && { generatorSendType }),
|
|
2062
2065
|
});
|
|
2063
2066
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
2064
|
-
localVarTypesStack.push(new
|
|
2067
|
+
localVarTypesStack.push(new ReceiverTypeMap(localVarTypes));
|
|
2065
2068
|
declaredVarTypesStack.push(new Map(declaredVarTypes));
|
|
2066
2069
|
localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
|
|
2067
2070
|
localVarUnionTypesStack.push(new Map(localVarUnionTypes));
|
|
@@ -2075,7 +2078,7 @@ function findCallsInCode(code, parser) {
|
|
|
2075
2078
|
constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
|
|
2076
2079
|
withBindingVarsStack.push(new Set(withBindingVars));
|
|
2077
2080
|
memberAliasesStack.push(new Map(memberAliases));
|
|
2078
|
-
classValueAliasesStack.push(new
|
|
2081
|
+
classValueAliasesStack.push(new ReceiverTypeMap(classValueAliases));
|
|
2079
2082
|
aliasesStack.push(new Map(aliases));
|
|
2080
2083
|
nonCallableNamesStack.push(new Set(nonCallableNames));
|
|
2081
2084
|
const body = node.childForFieldName('body');
|
|
@@ -2083,7 +2086,7 @@ function findCallsInCode(code, parser) {
|
|
|
2083
2086
|
if (functionParameterBindsName(node, name) ||
|
|
2084
2087
|
(body && pythonScopeBindsName(body, name))) continue;
|
|
2085
2088
|
if (!localVarTypes.has(name)) {
|
|
2086
|
-
localVarTypes.set(name, ctor.type);
|
|
2089
|
+
localVarTypes.set(name, ctor.type, 'constructor');
|
|
2087
2090
|
constructedReceiverVars.add(name);
|
|
2088
2091
|
}
|
|
2089
2092
|
}
|
|
@@ -2131,7 +2134,7 @@ function findCallsInCode(code, parser) {
|
|
|
2131
2134
|
? 'dict'
|
|
2132
2135
|
: (parameterPattern?.type === 'list_splat_pattern' ? 'tuple' : typeName);
|
|
2133
2136
|
if (receiverType && !['self', 'cls'].includes(nameNode.text)) {
|
|
2134
|
-
localVarTypes.set(nameNode.text, receiverType);
|
|
2137
|
+
localVarTypes.set(nameNode.text, receiverType, 'annotation', typeNode);
|
|
2135
2138
|
declaredVarTypes.set(nameNode.text, receiverType);
|
|
2136
2139
|
// The annotation's module qualifier is identity (fix
|
|
2137
2140
|
// #286e) — splat params got a builtin type, no qualifier.
|
|
@@ -2165,10 +2168,10 @@ function findCallsInCode(code, parser) {
|
|
|
2165
2168
|
const names = patternIdentifiers(left);
|
|
2166
2169
|
if (names.length === itemTypes.length) {
|
|
2167
2170
|
for (let i = 0; i < names.length; i++) {
|
|
2168
|
-
if (itemTypes[i]) localVarTypes.set(names[i], itemTypes[i]);
|
|
2171
|
+
if (itemTypes[i]) localVarTypes.set(names[i], itemTypes[i], 'flow', node);
|
|
2169
2172
|
}
|
|
2170
2173
|
} else if (names.length === 1 && itemTypes.length === 1) {
|
|
2171
|
-
localVarTypes.set(names[0], itemTypes[0]);
|
|
2174
|
+
localVarTypes.set(names[0], itemTypes[0], 'flow', node);
|
|
2172
2175
|
}
|
|
2173
2176
|
} else {
|
|
2174
2177
|
const source = iterableAttributeSource(right, localVarTypes);
|
|
@@ -2200,7 +2203,7 @@ function findCallsInCode(code, parser) {
|
|
|
2200
2203
|
const constructorNode = ctx.childForFieldName('function');
|
|
2201
2204
|
const exactCtor = exactConstructorInfo(constructorNode);
|
|
2202
2205
|
if (exactCtor) {
|
|
2203
|
-
localVarTypes.set(targetId.text, exactCtor.type);
|
|
2206
|
+
localVarTypes.set(targetId.text, exactCtor.type, 'constructor', node);
|
|
2204
2207
|
constructedReceiverVars.add(targetId.text);
|
|
2205
2208
|
if (exactCtor.qualifier &&
|
|
2206
2209
|
moduleAliases.has(exactCtor.qualifier.split('.')[0])) {
|
|
@@ -2214,7 +2217,7 @@ function findCallsInCode(code, parser) {
|
|
|
2214
2217
|
// Builtin open() is its own context value and always
|
|
2215
2218
|
// yields an IO object. The exact text/binary subtype
|
|
2216
2219
|
// is irrelevant for method-owner exclusion.
|
|
2217
|
-
localVarTypes.set(targetId.text, 'IO');
|
|
2220
|
+
localVarTypes.set(targetId.text, 'IO', 'with-binding', node);
|
|
2218
2221
|
}
|
|
2219
2222
|
}
|
|
2220
2223
|
}
|
|
@@ -2233,7 +2236,8 @@ function findCallsInCode(code, parser) {
|
|
|
2233
2236
|
if (rightFunction?.type === 'attribute' &&
|
|
2234
2237
|
rightFunction.childForFieldName('object')?.type === 'identifier' &&
|
|
2235
2238
|
rightFunction.childForFieldName('object').text === left.text) {
|
|
2236
|
-
assignmentRhsReceiverTypes.set(right.id, previousType
|
|
2239
|
+
assignmentRhsReceiverTypes.set(right.id, { type: previousType,
|
|
2240
|
+
...localVarTypes.fields(left.text, previousType) });
|
|
2237
2241
|
}
|
|
2238
2242
|
}
|
|
2239
2243
|
// Track type annotation: x: Foo = ... → x is Foo
|
|
@@ -2245,7 +2249,7 @@ function findCallsInCode(code, parser) {
|
|
|
2245
2249
|
const unionTypes = typeNamesFromAnnotation(typeNode);
|
|
2246
2250
|
const itemTypes = iterableBindingTypes(typeNode);
|
|
2247
2251
|
if (typeName) {
|
|
2248
|
-
localVarTypes.set(left.text, typeName);
|
|
2252
|
+
localVarTypes.set(left.text, typeName, 'constructor', right);
|
|
2249
2253
|
declaredVarTypes.set(left.text, typeName);
|
|
2250
2254
|
}
|
|
2251
2255
|
if (unionTypes.length > 1) localVarUnionTypes.set(left.text, unionTypes);
|
|
@@ -2277,7 +2281,7 @@ function findCallsInCode(code, parser) {
|
|
|
2277
2281
|
// valid for every subsequent assignment in type-correct
|
|
2278
2282
|
// code (`boundary: bytes | None; boundary = make()`).
|
|
2279
2283
|
const declaredType = declaredVarTypes.get(left.text);
|
|
2280
|
-
if (declaredType) localVarTypes.set(left.text, declaredType);
|
|
2284
|
+
if (declaredType) localVarTypes.set(left.text, declaredType, 'annotation', node);
|
|
2281
2285
|
} else {
|
|
2282
2286
|
// An annotation is the authoritative type source; a
|
|
2283
2287
|
// previous constructor qualifier must not survive it —
|
|
@@ -2318,13 +2322,13 @@ function findCallsInCode(code, parser) {
|
|
|
2318
2322
|
if (selfBranch && literalType &&
|
|
2319
2323
|
(previousType === literalType ||
|
|
2320
2324
|
compatible[previousType]?.has(literalType))) {
|
|
2321
|
-
localVarTypes.set(left.text, previousType);
|
|
2325
|
+
localVarTypes.set(left.text, previousType, 'flow', node);
|
|
2322
2326
|
}
|
|
2323
2327
|
}
|
|
2324
2328
|
if (!typeNode && right?.type === 'yield') {
|
|
2325
2329
|
const sendType = functionStack[
|
|
2326
2330
|
functionStack.length - 1]?.generatorSendType;
|
|
2327
|
-
if (sendType) localVarTypes.set(left.text, sendType);
|
|
2331
|
+
if (sendType) localVarTypes.set(left.text, sendType, 'flow', node);
|
|
2328
2332
|
}
|
|
2329
2333
|
// Literal assignment types the variable (fix #218):
|
|
2330
2334
|
// ansi_bytes = b"…" → bytes; out = [] → list. Compiler-true,
|
|
@@ -2332,7 +2336,7 @@ function findCallsInCode(code, parser) {
|
|
|
2332
2336
|
if (!typeNode && right && PY_LITERAL_RECEIVER_TYPES[right.type]) {
|
|
2333
2337
|
let litType = PY_LITERAL_RECEIVER_TYPES[right.type];
|
|
2334
2338
|
if (litType === 'str' && /^[rRuU]*[bB]/.test(right.text)) litType = 'bytes';
|
|
2335
|
-
localVarTypes.set(left.text, litType);
|
|
2339
|
+
localVarTypes.set(left.text, litType, 'literal', right);
|
|
2336
2340
|
}
|
|
2337
2341
|
if (right?.type === 'dictionary') {
|
|
2338
2342
|
const valueTypes = new Map();
|
|
@@ -2368,7 +2372,7 @@ function findCallsInCode(code, parser) {
|
|
|
2368
2372
|
const roundTripType = roundTripSource
|
|
2369
2373
|
? localVarTypes.get(roundTripSource) : null;
|
|
2370
2374
|
if (roundTripType && moduleAliases.has('pickle')) {
|
|
2371
|
-
localVarTypes.set(left.text, roundTripType);
|
|
2375
|
+
localVarTypes.set(left.text, roundTripType, 'flow', node);
|
|
2372
2376
|
localVarStdlibContracts.set(left.text, 'pickle');
|
|
2373
2377
|
}
|
|
2374
2378
|
if (right?.type === 'identifier') {
|
|
@@ -2376,7 +2380,7 @@ function findCallsInCode(code, parser) {
|
|
|
2376
2380
|
const classValue = !typeNode && isDirectScopeAssignment(node)
|
|
2377
2381
|
? exactConstructorInfo(right) : null;
|
|
2378
2382
|
if (classValue) {
|
|
2379
|
-
classValueAliases.set(left.text, classValue.type);
|
|
2383
|
+
classValueAliases.set(left.text, classValue.type, 'type-qualified', node);
|
|
2380
2384
|
}
|
|
2381
2385
|
}
|
|
2382
2386
|
// Member-access alias (fix #218): append = output.append
|
|
@@ -2433,7 +2437,7 @@ function findCallsInCode(code, parser) {
|
|
|
2433
2437
|
const constructorNode = right.childForFieldName('function');
|
|
2434
2438
|
const exactCtor = exactConstructorInfo(constructorNode);
|
|
2435
2439
|
if (exactCtor) {
|
|
2436
|
-
localVarTypes.set(left.text, exactCtor.type);
|
|
2440
|
+
localVarTypes.set(left.text, exactCtor.type, 'constructor', node);
|
|
2437
2441
|
constructedReceiverVars.add(left.text);
|
|
2438
2442
|
if (exactCtor.qualifier &&
|
|
2439
2443
|
moduleAliases.has(exactCtor.qualifier.split('.')[0])) {
|
|
@@ -2513,12 +2517,13 @@ function findCallsInCode(code, parser) {
|
|
|
2513
2517
|
const recvIsModule = !!memberAlias.receiver && moduleAliases.has(memberAlias.receiver) &&
|
|
2514
2518
|
!localVarTypes.has(memberAlias.receiver);
|
|
2515
2519
|
calls.push({
|
|
2520
|
+
callSite: typeOrigin('call', funcNode),
|
|
2516
2521
|
name: memberAlias.attr,
|
|
2517
2522
|
line: node.startPosition.row + 1,
|
|
2518
2523
|
isMethod: true,
|
|
2519
2524
|
aliasCall: true,
|
|
2520
2525
|
...(memberAlias.receiver && { receiver: memberAlias.receiver }),
|
|
2521
|
-
...(recvType && { receiverType: recvType }),
|
|
2526
|
+
...(recvType && { receiverType: recvType, ...localVarTypes.fields(memberAlias.receiver, recvType) }),
|
|
2522
2527
|
...(recvIsModule && { receiverIsModule: true }),
|
|
2523
2528
|
...(assignedTo && { assignedTo }),
|
|
2524
2529
|
...assignedIterFields,
|
|
@@ -2533,6 +2538,7 @@ function findCallsInCode(code, parser) {
|
|
|
2533
2538
|
const resolvedName = aliases.get(funcNode.text);
|
|
2534
2539
|
const firstArg = getFirstStringArg(node);
|
|
2535
2540
|
calls.push({
|
|
2541
|
+
callSite: typeOrigin('call', funcNode),
|
|
2536
2542
|
name: funcNode.text,
|
|
2537
2543
|
...(resolvedName && { resolvedName }),
|
|
2538
2544
|
line: node.startPosition.row + 1,
|
|
@@ -2649,15 +2655,16 @@ function findCallsInCode(code, parser) {
|
|
|
2649
2655
|
source.rootTypeQualifier;
|
|
2650
2656
|
}
|
|
2651
2657
|
}
|
|
2658
|
+
const narrowedType = receiver && narrowedReceiverType(
|
|
2659
|
+
objNode, receiver, localVarUnionTypes.get(receiver));
|
|
2660
|
+
const comprehensionType = receiver && comprehensionReceiverType(
|
|
2661
|
+
objNode, receiver, localIterableTypes, callableIterableTypes, instanceFieldContracts);
|
|
2662
|
+
const rhsReceiver = assignmentRhsReceiverTypes.get(node.id);
|
|
2652
2663
|
const receiverType = receiver
|
|
2653
|
-
? (
|
|
2654
|
-
objNode, receiver, localVarUnionTypes.get(receiver)) ||
|
|
2655
|
-
comprehensionReceiverType(
|
|
2656
|
-
objNode, receiver, localIterableTypes,
|
|
2657
|
-
callableIterableTypes, instanceFieldContracts) ||
|
|
2664
|
+
? (narrowedType || comprehensionType ||
|
|
2658
2665
|
localVarTypes.get(receiver) ||
|
|
2659
2666
|
classValueAliases.get(receiver))
|
|
2660
|
-
||
|
|
2667
|
+
|| rhsReceiver?.type
|
|
2661
2668
|
: (subscriptReceiverType ||
|
|
2662
2669
|
(objNode ? PY_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
|
|
2663
2670
|
let iterationSource = receiver
|
|
@@ -2705,6 +2712,7 @@ function findCallsInCode(code, parser) {
|
|
|
2705
2712
|
const capabilityGuard = receiverCapabilityGuard(
|
|
2706
2713
|
node, objNode, attrNode.text);
|
|
2707
2714
|
calls.push({
|
|
2715
|
+
callSite: typeOrigin('call', attrNode),
|
|
2708
2716
|
name: attrNode.text,
|
|
2709
2717
|
// Multi-line chains (obj.x()\n.y()) must report each
|
|
2710
2718
|
// method's OWN name line, not the chain-start line —
|
|
@@ -2712,7 +2720,22 @@ function findCallsInCode(code, parser) {
|
|
|
2712
2720
|
line: attrNode.startPosition.row + 1,
|
|
2713
2721
|
isMethod: true,
|
|
2714
2722
|
receiver,
|
|
2715
|
-
...(receiverType && { receiverType
|
|
2723
|
+
...(receiverType && { receiverType,
|
|
2724
|
+
...(narrowedType || comprehensionType ? {
|
|
2725
|
+
receiverTypeSource: 'flow', receiverTypeEvidence: {
|
|
2726
|
+
...typeOrigin('flow', objNode),
|
|
2727
|
+
union: localVarUnionTypes.get(receiver),
|
|
2728
|
+
...(comprehensionType && { type: comprehensionType, comprehension: true }),
|
|
2729
|
+
},
|
|
2730
|
+
} : receiver && classValueAliases.get(receiver) === receiverType ?
|
|
2731
|
+
classValueAliases.fields(receiver) :
|
|
2732
|
+
receiver && !localVarTypes.has(receiver) && rhsReceiver?.type === receiverType
|
|
2733
|
+
? { receiverTypeSource: rhsReceiver.receiverTypeSource,
|
|
2734
|
+
receiverTypeEvidence: rhsReceiver.receiverTypeEvidence } :
|
|
2735
|
+
receiver ? localVarTypes.fields(receiver, receiverType) : {
|
|
2736
|
+
receiverTypeSource: 'literal', receiverTypeEvidence: typeOrigin('literal', objNode),
|
|
2737
|
+
}),
|
|
2738
|
+
}),
|
|
2716
2739
|
...(receiver && localVarStdlibContracts.has(receiver) && {
|
|
2717
2740
|
receiverTypeStdlibModule:
|
|
2718
2741
|
localVarStdlibContracts.get(receiver),
|
|
@@ -2817,7 +2840,7 @@ function findCallsInCode(code, parser) {
|
|
|
2817
2840
|
line: mvAttr.startPosition.row + 1,
|
|
2818
2841
|
isMethod: true,
|
|
2819
2842
|
receiver: mvObj.text,
|
|
2820
|
-
...(mvType && { receiverType: mvType }),
|
|
2843
|
+
...(mvType && { receiverType: mvType, ...localVarTypes.fields(mvObj.text, mvType) }),
|
|
2821
2844
|
isFunctionReference: true,
|
|
2822
2845
|
enclosingFunction
|
|
2823
2846
|
});
|
|
@@ -2858,8 +2881,7 @@ function findCallsInCode(code, parser) {
|
|
|
2858
2881
|
// Restore localVarTypes to pre-function state
|
|
2859
2882
|
const saved = localVarTypesStack.pop();
|
|
2860
2883
|
if (saved) {
|
|
2861
|
-
localVarTypes.
|
|
2862
|
-
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
2884
|
+
localVarTypes.restore(saved);
|
|
2863
2885
|
}
|
|
2864
2886
|
const savedDeclared = declaredVarTypesStack.pop();
|
|
2865
2887
|
if (savedDeclared) {
|
|
@@ -2924,7 +2946,7 @@ function findCallsInCode(code, parser) {
|
|
|
2924
2946
|
if (savedClassValueAliases) {
|
|
2925
2947
|
classValueAliases.clear();
|
|
2926
2948
|
for (const [k, v] of savedClassValueAliases) {
|
|
2927
|
-
classValueAliases.set(k, v);
|
|
2949
|
+
classValueAliases.set(k, v, savedClassValueAliases.origins.get(k));
|
|
2928
2950
|
}
|
|
2929
2951
|
}
|
|
2930
2952
|
const savedCallableAliases = aliasesStack.pop();
|
|
@@ -3485,6 +3507,10 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3485
3507
|
if (!body) return false;
|
|
3486
3508
|
|
|
3487
3509
|
const attrTypes = new Map();
|
|
3510
|
+
const attrOrigins = new Map();
|
|
3511
|
+
Object.defineProperty(attrTypes, 'origins', { value: attrOrigins });
|
|
3512
|
+
const externalFlows = new Map();
|
|
3513
|
+
Object.defineProperty(attrTypes, 'externalFlows', { value: externalFlows });
|
|
3488
3514
|
for (const [field, contract] of explicitContracts.get(className) || []) {
|
|
3489
3515
|
if (contract.type) attrTypes.set(field, contract.type);
|
|
3490
3516
|
}
|
|
@@ -3657,6 +3683,8 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3657
3683
|
// old constructor-name heuristic to arbitrary methods. Unknown or
|
|
3658
3684
|
// conflicting assignments leave the field untyped.
|
|
3659
3685
|
const runtimeFieldTypes = new Map();
|
|
3686
|
+
const runtimeFieldWrites = new Map();
|
|
3687
|
+
const externalFieldWrites = new Map();
|
|
3660
3688
|
const invalidRuntimeFields = new Set();
|
|
3661
3689
|
for (let i = 0; i < body.namedChildCount; i++) {
|
|
3662
3690
|
let member = body.namedChild(i);
|
|
@@ -3671,12 +3699,18 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3671
3699
|
traverseTree(memberBody, stmt => {
|
|
3672
3700
|
if (stmt.type !== 'expression_statement') return true;
|
|
3673
3701
|
const assignment = stmt.firstChild;
|
|
3674
|
-
if (assignment
|
|
3702
|
+
if (!['assignment', 'augmented_assignment'].includes(assignment?.type)) return true;
|
|
3675
3703
|
const left = assignment.childForFieldName('left');
|
|
3676
3704
|
const field = left?.type === 'attribute' &&
|
|
3677
3705
|
left.childForFieldName('object')?.text === 'self'
|
|
3678
3706
|
? left.childForFieldName('attribute')?.text : null;
|
|
3679
3707
|
if (!field) return true;
|
|
3708
|
+
let enclosing = assignment.parent;
|
|
3709
|
+
while (enclosing && !['function_definition', 'lambda', 'class_definition'].includes(enclosing.type)) enclosing = enclosing.parent;
|
|
3710
|
+
if (assignment.type !== 'assignment' || enclosing !== member) {
|
|
3711
|
+
invalidRuntimeFields.add(field);
|
|
3712
|
+
return true;
|
|
3713
|
+
}
|
|
3680
3714
|
const right = assignment.childForFieldName('right');
|
|
3681
3715
|
const callable = right?.type === 'call'
|
|
3682
3716
|
? right.childForFieldName('function') : null;
|
|
@@ -3692,11 +3726,35 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3692
3726
|
? callable.childForFieldName('attribute')?.text : null;
|
|
3693
3727
|
const resolveCallType = options.resolveCallType ||
|
|
3694
3728
|
options.resolveBuiltinCallType;
|
|
3695
|
-
const
|
|
3729
|
+
const literalType = right && ['dictionary', 'dictionary_comprehension', 'list',
|
|
3730
|
+
'list_comprehension', 'set', 'set_comprehension', 'tuple'].includes(right.type)
|
|
3731
|
+
? PY_LITERAL_RECEIVER_TYPES[right.type] : null;
|
|
3732
|
+
const importedCall = callable?.type === 'identifier' &&
|
|
3733
|
+
!isPythonNameShadowedAt(callable, callable.text)
|
|
3734
|
+
? options.resolveImportedCallType?.(callable.text) : null;
|
|
3735
|
+
const runtimeType = literalType || importedCall?.type || directConstructor ||
|
|
3696
3736
|
(moduleName && functionName &&
|
|
3697
3737
|
!isPythonNameShadowedAt(callable, moduleName)
|
|
3698
3738
|
? resolveCallType?.(moduleName, functionName)
|
|
3699
3739
|
: null);
|
|
3740
|
+
let producer = right, depth = 0;
|
|
3741
|
+
while (producer?.type === 'call') {
|
|
3742
|
+
depth++;
|
|
3743
|
+
producer = producer.childForFieldName('function');
|
|
3744
|
+
}
|
|
3745
|
+
const factoryPath = producer?.type === 'attribute' &&
|
|
3746
|
+
producer.childForFieldName('object')?.type === 'identifier'
|
|
3747
|
+
? [producer.childForFieldName('object').text, producer.childForFieldName('attribute')?.text]
|
|
3748
|
+
: producer?.type === 'identifier' ? [producer.text] : null;
|
|
3749
|
+
const externalFactory = !runtimeType && depth > 0 && depth <= 3 && factoryPath?.every(Boolean) &&
|
|
3750
|
+
!isPythonNameShadowedAt(producer, factoryPath[0])
|
|
3751
|
+
? options.resolveExternalFactory?.(factoryPath) : null;
|
|
3752
|
+
if (externalFactory) {
|
|
3753
|
+
if (!externalFieldWrites.has(field)) externalFieldWrites.set(field, []);
|
|
3754
|
+
externalFieldWrites.get(field).push({ ...externalFactory, path: factoryPath, depth,
|
|
3755
|
+
assignment: typeOrigin('field', assignment), expression: typeOrigin('field', right) });
|
|
3756
|
+
return true;
|
|
3757
|
+
}
|
|
3700
3758
|
if (!runtimeType) {
|
|
3701
3759
|
// None is a harmless uninitialized state; any other
|
|
3702
3760
|
// unknown write could replace the field with a project
|
|
@@ -3706,18 +3764,32 @@ function findInstanceAttributeTypes(code, parser, options = {}) {
|
|
|
3706
3764
|
}
|
|
3707
3765
|
if (!runtimeFieldTypes.has(field)) runtimeFieldTypes.set(field, new Set());
|
|
3708
3766
|
runtimeFieldTypes.get(field).add(runtimeType);
|
|
3767
|
+
if (!runtimeFieldWrites.has(field)) runtimeFieldWrites.set(field, []);
|
|
3768
|
+
runtimeFieldWrites.get(field).push({ type: runtimeType,
|
|
3769
|
+
assignment: typeOrigin('field', assignment), expression: typeOrigin('field', right),
|
|
3770
|
+
...(literalType ? { literal: right.type } : importedCall ? { importedCall } : { legacy: true }) });
|
|
3709
3771
|
return true;
|
|
3710
3772
|
});
|
|
3711
3773
|
}
|
|
3774
|
+
for (const [field, assignments] of externalFieldWrites) {
|
|
3775
|
+
if (!invalidRuntimeFields.has(field) && !runtimeFieldTypes.has(field) && !attrTypes.has(field) &&
|
|
3776
|
+
new Set(assignments.map(write => `${write.module}.${write.producer}:${write.depth}`)).size === 1) {
|
|
3777
|
+
externalFlows.set(field, { field, assignments });
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3712
3780
|
for (const [field, types] of runtimeFieldTypes) {
|
|
3713
3781
|
if (invalidRuntimeFields.has(field) || types.size !== 1) continue;
|
|
3714
3782
|
const type = [...types][0];
|
|
3715
3783
|
if (!attrTypes.has(field) || attrTypes.get(field) === type) {
|
|
3716
3784
|
attrTypes.set(field, type);
|
|
3785
|
+
const assignments = runtimeFieldWrites.get(field);
|
|
3786
|
+
if (assignments.every(write => !write.legacy)) {
|
|
3787
|
+
attrOrigins.set(field, { assignments });
|
|
3788
|
+
}
|
|
3717
3789
|
}
|
|
3718
3790
|
}
|
|
3719
3791
|
|
|
3720
|
-
if (attrTypes.size > 0) {
|
|
3792
|
+
if (attrTypes.size > 0 || externalFlows.size > 0) {
|
|
3721
3793
|
result.set(className, attrTypes);
|
|
3722
3794
|
}
|
|
3723
3795
|
|
|
@@ -3816,6 +3888,98 @@ function isEntryPoint(symbol) {
|
|
|
3816
3888
|
return getEntryPointKind(symbol) !== null;
|
|
3817
3889
|
}
|
|
3818
3890
|
|
|
3891
|
+
/** Module-level pytest declarations. Decorator ownership is resolved by the
|
|
3892
|
+
* index; a decorator merely spelled `fixture` is not sufficient evidence.
|
|
3893
|
+
*/
|
|
3894
|
+
function findPythonModuleEvidence(code, parser) {
|
|
3895
|
+
const tree = parseTree(parser, code), references = [], wildcards = [];
|
|
3896
|
+
traverseTree(tree.rootNode, node => {
|
|
3897
|
+
if (node.type === 'identifier' && node.text === '__all__') references.push(node);
|
|
3898
|
+
if (node.type === 'import_from_statement' && node.namedChildren.some(child => child.type === 'wildcard_import')) {
|
|
3899
|
+
const module = node.childForFieldName('module_name');
|
|
3900
|
+
wildcards.push({ name: '*', module: module?.text, origin: typeOrigin('import', node),
|
|
3901
|
+
topLevel: node.parent?.type === 'module' });
|
|
3902
|
+
}
|
|
3903
|
+
return true;
|
|
3904
|
+
});
|
|
3905
|
+
let exports = null;
|
|
3906
|
+
if (references.length === 1) {
|
|
3907
|
+
const name = references[0], assignment = name.parent, value = assignment.childForFieldName('right');
|
|
3908
|
+
if (assignment.type === 'assignment' && assignment.childForFieldName('left') === name &&
|
|
3909
|
+
assignment.parent?.parent?.type === 'module' && ['list', 'tuple'].includes(value?.type)) {
|
|
3910
|
+
const literals = value.namedChildren.map(item => {
|
|
3911
|
+
const content = item.type === 'string' && item.namedChildren.find(child => child.type === 'string_content');
|
|
3912
|
+
return content && item.namedChildren.every(child => ['string_start', 'string_content', 'string_end'].includes(child.type))
|
|
3913
|
+
? { value: content.text, origin: typeOrigin('literal', item) } : null;
|
|
3914
|
+
});
|
|
3915
|
+
if (literals.every(Boolean)) exports = { name: '__all__', origin: typeOrigin('assignment', assignment),
|
|
3916
|
+
literals, otherReferences: [] };
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return { exports, wildcards };
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
function findPytestFunctions(code, parser) {
|
|
3923
|
+
const tree = parseTree(parser, code);
|
|
3924
|
+
const pathOf = node => {
|
|
3925
|
+
if (node?.type === 'identifier') return [node.text];
|
|
3926
|
+
if (node?.type !== 'attribute') return null;
|
|
3927
|
+
const root = pathOf(node.childForFieldName('object'));
|
|
3928
|
+
const member = node.childForFieldName('attribute');
|
|
3929
|
+
return root && member ? [...root, member.text] : null;
|
|
3930
|
+
};
|
|
3931
|
+
const out = [];
|
|
3932
|
+
for (const outer of tree.rootNode.namedChildren) {
|
|
3933
|
+
const node = outer.type === 'decorated_definition' ? outer.childForFieldName('definition') : outer;
|
|
3934
|
+
if (node?.type !== 'function_definition') continue;
|
|
3935
|
+
const body = node.childForFieldName('body');
|
|
3936
|
+
const params = node.childForFieldName('parameters');
|
|
3937
|
+
const decorators = outer.type === 'decorated_definition'
|
|
3938
|
+
? outer.namedChildren.filter(child => child.type === 'decorator').map(decorator => {
|
|
3939
|
+
const expression = decorator.namedChild(0);
|
|
3940
|
+
const callable = expression?.type === 'call' ? expression.childForFieldName('function') : expression;
|
|
3941
|
+
let alias, dynamicName = false;
|
|
3942
|
+
for (const argument of expression?.childForFieldName('arguments')?.namedChildren || []) {
|
|
3943
|
+
if (argument.type !== 'keyword_argument' || argument.childForFieldName('name')?.text !== 'name') continue;
|
|
3944
|
+
const value = argument.childForFieldName('value');
|
|
3945
|
+
const content = value?.type === 'string' && value.namedChildren.find(child => child.type === 'string_content');
|
|
3946
|
+
if (content && value.namedChildren.every(child => ['string_start', 'string_content', 'string_end'].includes(child.type))) alias = content.text;
|
|
3947
|
+
else dynamicName = true;
|
|
3948
|
+
}
|
|
3949
|
+
return { path: pathOf(callable), alias, dynamicName, origin: typeOrigin('fixture', decorator) };
|
|
3950
|
+
}) : [];
|
|
3951
|
+
let yields = false;
|
|
3952
|
+
traverseTree(body, child => {
|
|
3953
|
+
if (child.type === 'function_definition' || child.type === 'lambda' || child.type === 'class_definition') return false;
|
|
3954
|
+
if (child.type === 'yield') yields = true;
|
|
3955
|
+
return true;
|
|
3956
|
+
});
|
|
3957
|
+
const annotation = node.childForFieldName('return_type');
|
|
3958
|
+
let value = annotation?.namedChild(0);
|
|
3959
|
+
let container;
|
|
3960
|
+
if (yields) {
|
|
3961
|
+
if (value?.type === 'subscript') {
|
|
3962
|
+
container = pathOf(value.childForFieldName('value'));
|
|
3963
|
+
value = value.childForFieldName('subscript');
|
|
3964
|
+
} else if (value?.type === 'generic_type') {
|
|
3965
|
+
container = pathOf(value.namedChild(0));
|
|
3966
|
+
value = value.namedChildren.find(child => child.type === 'type_parameter')?.namedChild(0)?.namedChild(0);
|
|
3967
|
+
} else value = null;
|
|
3968
|
+
}
|
|
3969
|
+
const valuePath = pathOf(value);
|
|
3970
|
+
out.push({ name: node.childForFieldName('name')?.text,
|
|
3971
|
+
startLine: outer.startPosition.row + 1, endLine: outer.endPosition.row + 1,
|
|
3972
|
+
async: node.children.some(child => child.type === 'async'), decorators, yields,
|
|
3973
|
+
parameters: (params?.namedChildren || []).filter(param => param.type === 'identifier')
|
|
3974
|
+
.map(param => ({ name: param.text, origin: typeOrigin('fixture', param),
|
|
3975
|
+
unchanged: !pythonScopeBindsName(body, param.text) })),
|
|
3976
|
+
returnType: annotation?.text, valuePath, container,
|
|
3977
|
+
returnOrigin: annotation ? typeOrigin('annotation', annotation) : null,
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
return out;
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3819
3983
|
// Stable CPython/stdlib runtime contracts. These are intentionally small and
|
|
3820
3984
|
// language-owned: callers must still prove that the import is external (not a
|
|
3821
3985
|
// project module with the same name) before using them as exclusion evidence.
|
|
@@ -3826,6 +3990,7 @@ const PY_BUILTIN_CALL_RETURNS = Object.freeze({
|
|
|
3826
3990
|
'json.dumps': 'str',
|
|
3827
3991
|
'os.urandom': 'bytes',
|
|
3828
3992
|
'urllib.request.getproxies': 'dict',
|
|
3993
|
+
'urllib.parse.parse_qs': 'dict',
|
|
3829
3994
|
'zlib.compressobj': 'ZlibCompress',
|
|
3830
3995
|
'zlib.decompressobj': 'ZlibDecompress',
|
|
3831
3996
|
});
|
|
@@ -3843,6 +4008,7 @@ function getBuiltinFieldType(moduleName, fieldName) {
|
|
|
3843
4008
|
}
|
|
3844
4009
|
|
|
3845
4010
|
module.exports = {
|
|
4011
|
+
findPythonModuleEvidence,
|
|
3846
4012
|
findFunctions,
|
|
3847
4013
|
findClasses,
|
|
3848
4014
|
findStateObjects,
|
|
@@ -3853,6 +4019,7 @@ module.exports = {
|
|
|
3853
4019
|
findInstanceAttributeTypes,
|
|
3854
4020
|
getBuiltinCallReturnType,
|
|
3855
4021
|
getBuiltinFieldType,
|
|
4022
|
+
findPytestFunctions,
|
|
3856
4023
|
isEntryPoint,
|
|
3857
4024
|
getEntryPointKind,
|
|
3858
4025
|
parse
|