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.
@@ -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 Map(); // Track local variable types: varName -> typeName (for receiverType inference)
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 receiver type
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 Map();
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 Map(localVarTypes));
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 Map(classValueAliases));
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
- ? (narrowedReceiverType(
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
- || assignmentRhsReceiverTypes.get(node.id)
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.clear();
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?.type !== 'assignment') return true;
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 runtimeType = directConstructor ||
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