ucn 5.2.0 → 5.2.2
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 +17 -3
- package/.claude/skills/ucn/references/commands.md +4 -4
- package/README.md +17 -5
- package/cli/index.js +3 -0
- package/core/accessors.js +183 -0
- package/core/account.js +36 -8
- package/core/analysis.js +46 -0
- package/core/ast-analysis.js +104 -0
- package/core/cache.js +150 -9
- package/core/callers.js +1196 -122
- package/core/command-contracts.js +13 -13
- package/core/deadcode.js +41 -2
- package/core/execute.js +4 -1
- package/core/graph.js +72 -5
- package/core/index-ir.js +15 -3
- package/core/ir.js +58 -9
- package/core/output/analysis.js +30 -1
- package/core/output/graph.js +28 -8
- package/core/output/public.js +4 -0
- package/core/output/refactoring.js +31 -2
- package/core/output/reporting.js +7 -0
- package/core/project.js +88 -3
- package/core/verify.js +85 -3
- package/languages/c-family.js +27 -34
- package/languages/csharp.js +26 -1
- package/languages/go.js +170 -42
- package/languages/javascript.js +258 -10
- package/languages/python.js +591 -32
- package/languages/rust.js +1 -0
- package/package.json +1 -1
package/core/callers.js
CHANGED
|
@@ -20,6 +20,15 @@ const CONSTRUCTABLE_BINDING_KINDS = new Set([
|
|
|
20
20
|
const RESERVED_RECEIVER_NAMES = new Set([
|
|
21
21
|
'self', 'cls', 'this', 'super', 'base', 'Self',
|
|
22
22
|
]);
|
|
23
|
+
const CROSS_OPERATION_FLOW_CACHE_LIMIT = 4096;
|
|
24
|
+
|
|
25
|
+
// `base` is a contextual receiver keyword only in C#. TypeScript and the
|
|
26
|
+
// other supported languages may bind an ordinary local named base; treating
|
|
27
|
+
// it as reserved globally suppresses otherwise exact assignment flow.
|
|
28
|
+
function _isReservedReceiver(language, receiver) {
|
|
29
|
+
if (receiver === 'base') return language === 'csharp';
|
|
30
|
+
return RESERVED_RECEIVER_NAMES.has(receiver);
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
/** Set.some() helper — like Array.some() but for Sets */
|
|
25
34
|
function setSome(set, predicate) {
|
|
@@ -586,6 +595,7 @@ function findCallers(index, name, options = {}) {
|
|
|
586
595
|
const localTypeCache = new Map(); // `${filePath}:${startLine}` -> localTypes Map or null
|
|
587
596
|
const returnFlowCache = new Map(); // filePath -> return-type-flow map (see _buildReturnTypeFlowMap)
|
|
588
597
|
const foldCtxCache = new Map(); // filePath -> chained-receiver fold context (fix #258)
|
|
598
|
+
const pythonIndexedReceiverCache = new Map();
|
|
589
599
|
|
|
590
600
|
// Use inverted callee index to skip files that don't contain calls to this name
|
|
591
601
|
let calleeFiles = index.getCalleeFiles(name);
|
|
@@ -651,6 +661,26 @@ function findCallers(index, name, options = {}) {
|
|
|
651
661
|
continue;
|
|
652
662
|
}
|
|
653
663
|
|
|
664
|
+
if (fileEntry.language === 'go' && call.isMethod &&
|
|
665
|
+
!call.receiverType && call.receiverIndexField) {
|
|
666
|
+
const indexedType = _goIndexedReceiverType(index, filePath, call);
|
|
667
|
+
if (indexedType?.type) {
|
|
668
|
+
call = {
|
|
669
|
+
...call,
|
|
670
|
+
receiverType: indexedType.type,
|
|
671
|
+
...(indexedType.fromFile && {
|
|
672
|
+
receiverTypeFlowFile: indexedType.fromFile,
|
|
673
|
+
}),
|
|
674
|
+
};
|
|
675
|
+
} else if (indexedType?.externalVia) {
|
|
676
|
+
call = {
|
|
677
|
+
...call,
|
|
678
|
+
receiverExternalFlow: indexedType.externalVia,
|
|
679
|
+
receiverExternalConcreteFlow: true,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
654
684
|
// A call-shaped identifier in a C/C++ replacement list can
|
|
655
685
|
// be a macro parameter (`#define APPLY(fn, x) fn(x)`). It is
|
|
656
686
|
// dynamically supplied by each expansion and therefore is
|
|
@@ -870,7 +900,7 @@ function findCallers(index, name, options = {}) {
|
|
|
870
900
|
// type derives from OTHER files' annotations, so it must never be
|
|
871
901
|
// persisted with this file's calls.
|
|
872
902
|
if (call.isMethod && call.receiver &&
|
|
873
|
-
!
|
|
903
|
+
!_isReservedReceiver(fileEntry.language, call.receiver) &&
|
|
874
904
|
(!call.receiverType || call.receiverTypeGuessed) &&
|
|
875
905
|
!call.receiverPatternShadow && !call.receiverFlowInvalidated &&
|
|
876
906
|
!call.receiverIsChainRoot &&
|
|
@@ -916,6 +946,34 @@ function findCallers(index, name, options = {}) {
|
|
|
916
946
|
...(flowEntry.fromFile && { receiverTypeFlowFile: flowEntry.fromFile }) };
|
|
917
947
|
}
|
|
918
948
|
}
|
|
949
|
+
|
|
950
|
+
// Python indexed receivers (fix #324): `layout["body"].update()`
|
|
951
|
+
// dispatch through the container's compiler-visible
|
|
952
|
+
// `__getitem__` return contract. The parser retains only a
|
|
953
|
+
// simple identifier root; query time pins that root's type to
|
|
954
|
+
// an exact project definition before trusting the contract.
|
|
955
|
+
// External, unresolved, and ambiguous containers abstain.
|
|
956
|
+
if (fileEntry.language === 'python' && call.isMethod &&
|
|
957
|
+
!call.receiverType && call.receiverSubscriptRoot) {
|
|
958
|
+
const indexedType = _pythonIndexedReceiverType(
|
|
959
|
+
index, filePath, call, () => {
|
|
960
|
+
let flowMap = returnFlowCache.get(filePath);
|
|
961
|
+
if (flowMap === undefined) {
|
|
962
|
+
flowMap = _buildReturnTypeFlowMap(index, filePath, calls);
|
|
963
|
+
returnFlowCache.set(filePath, flowMap);
|
|
964
|
+
}
|
|
965
|
+
return flowMap;
|
|
966
|
+
}, pythonIndexedReceiverCache);
|
|
967
|
+
if (indexedType?.type) {
|
|
968
|
+
call = {
|
|
969
|
+
...call,
|
|
970
|
+
receiverType: indexedType.type,
|
|
971
|
+
...(indexedType.fromFile && {
|
|
972
|
+
receiverTypeFlowFile: indexedType.fromFile,
|
|
973
|
+
}),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
919
977
|
// Python loop/comprehension bindings can inherit item types
|
|
920
978
|
// from a declared attribute path. The parser records the
|
|
921
979
|
// source path (`request.headers.raw`) and tuple position;
|
|
@@ -1581,9 +1639,56 @@ function findCallers(index, name, options = {}) {
|
|
|
1581
1639
|
cbTargetDefs.some(d => d.file &&
|
|
1582
1640
|
_sameNominalPackageDir(path.dirname(d.file), path.dirname(filePath), fileEntry.language));
|
|
1583
1641
|
let cbImportLink = false;
|
|
1642
|
+
// A module-scoped variable passed as a callback has a
|
|
1643
|
+
// concrete lexical owner, but its VALUE may be dynamic
|
|
1644
|
+
// (`const app = express(); use(app)`). It must not borrow
|
|
1645
|
+
// target identity from an unrelated file-level import
|
|
1646
|
+
// edge. Exact named/default imports still confirm when
|
|
1647
|
+
// their export chain reaches the pin; definitive chains
|
|
1648
|
+
// elsewhere exclude; dynamic/CJS factory values remain
|
|
1649
|
+
// visible. Account-gated to preserve legacy trace/blast
|
|
1650
|
+
// behavior while strengthening grep-reliable surfaces.
|
|
1651
|
+
if (collectAccount && !cbSameFile && call.moduleLocalBinding &&
|
|
1652
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
1653
|
+
const cbNameBindings = (fileEntry.importBindings || []).filter(binding =>
|
|
1654
|
+
binding.name === call.name || binding.alias === call.name);
|
|
1655
|
+
let cbBindingReaches = false;
|
|
1656
|
+
let cbBindingUnknown = cbNameBindings.length === 0;
|
|
1657
|
+
for (const binding of cbNameBindings) {
|
|
1658
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
1659
|
+
if (!rel) {
|
|
1660
|
+
const mod = String(binding.module || '');
|
|
1661
|
+
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
1662
|
+
if (mod.startsWith('.') ||
|
|
1663
|
+
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
1664
|
+
cbBindingUnknown = true;
|
|
1665
|
+
}
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
const resolvedAbs = path.join(index.root, rel);
|
|
1669
|
+
const verdict = binding.defaultLike
|
|
1670
|
+
? _defaultBindingReaches(index, resolvedAbs, cbTargetFiles)
|
|
1671
|
+
: _nameBindingReaches(index, resolvedAbs, binding.name, cbTargetFiles);
|
|
1672
|
+
if (verdict === 'yes') {
|
|
1673
|
+
cbBindingReaches = true;
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
if (verdict === 'unknown') cbBindingUnknown = true;
|
|
1677
|
+
}
|
|
1678
|
+
if (!cbBindingReaches && !cbBindingUnknown) {
|
|
1679
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (!cbBindingReaches) {
|
|
1683
|
+
routeUnverified(filePath, fileEntry, call, 'ambiguous-binding', calledAs);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
cbImportLink = true;
|
|
1687
|
+
}
|
|
1584
1688
|
if (!cbSameFile && !cbSamePackage) {
|
|
1585
1689
|
const cbImports = index.importGraph.get(filePath);
|
|
1586
|
-
cbImportLink =
|
|
1690
|
+
cbImportLink = cbImportLink ||
|
|
1691
|
+
!!(cbImports && setSome(cbImports, imp => cbTargetFiles.has(imp)));
|
|
1587
1692
|
if (!cbImportLink && cbImports) {
|
|
1588
1693
|
for (const imp of cbImports) {
|
|
1589
1694
|
const trans = index.importGraph.get(imp);
|
|
@@ -2072,7 +2177,8 @@ function findCallers(index, name, options = {}) {
|
|
|
2072
2177
|
continue;
|
|
2073
2178
|
}
|
|
2074
2179
|
}
|
|
2075
|
-
} else if (['self', 'cls', 'this', 'super'
|
|
2180
|
+
} else if (['self', 'cls', 'this', 'super'].includes(call.receiver) ||
|
|
2181
|
+
(call.receiver === 'base' && fileEntry.language === 'csharp') ||
|
|
2076
2182
|
(call.receiver === 'Self' && fileEntry.language === 'rust')) {
|
|
2077
2183
|
// self/this/super.method() — resolve to same-class or parent method.
|
|
2078
2184
|
// Rust `Self::method()` (fix #232) is the path-call same-class form:
|
|
@@ -2089,8 +2195,8 @@ function findCallers(index, name, options = {}) {
|
|
|
2089
2195
|
}
|
|
2090
2196
|
} else {
|
|
2091
2197
|
// For super(), skip same-class — only check parent chain
|
|
2092
|
-
const parentOnlyReceiver =
|
|
2093
|
-
call.receiver === '
|
|
2198
|
+
const parentOnlyReceiver = call.receiver === 'super' ||
|
|
2199
|
+
(call.receiver === 'base' && fileEntry.language === 'csharp');
|
|
2094
2200
|
let matchedClass = !parentOnlyReceiver &&
|
|
2095
2201
|
definitions.some(d => d.className === callerSymbol.className)
|
|
2096
2202
|
? callerSymbol.className : null;
|
|
@@ -2244,7 +2350,23 @@ function findCallers(index, name, options = {}) {
|
|
|
2244
2350
|
...call,
|
|
2245
2351
|
receiver: call.receiverRoot,
|
|
2246
2352
|
});
|
|
2247
|
-
if (inferredRoot?.type)
|
|
2353
|
+
if (inferredRoot?.type) {
|
|
2354
|
+
fieldHopRootType = inferredRoot.type;
|
|
2355
|
+
} else if (inferredRoot?.externalVia &&
|
|
2356
|
+
!call.receiverExternalFlow) {
|
|
2357
|
+
// External root-flow through a field path (fix #306,
|
|
2358
|
+
// chi-measured): `resp := http.Get(...); resp.Body.Close()`.
|
|
2359
|
+
// The compiler fixed `resp` outside the project, so
|
|
2360
|
+
// `.Body` cannot turn a globally unique project Close
|
|
2361
|
+
// into an exact edge. Preserve the provenance on the
|
|
2362
|
+
// demote-only external-contract rail; a generic
|
|
2363
|
+
// external root may still carry a project value.
|
|
2364
|
+
call = {
|
|
2365
|
+
...call,
|
|
2366
|
+
receiverExternalFlow:
|
|
2367
|
+
`${inferredRoot.externalVia}.${call.receiverField}`,
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2248
2370
|
}
|
|
2249
2371
|
// Go's parser preserves the package qualifier on a declared
|
|
2250
2372
|
// root type (`r *http.Response; r.Body.Close()`). If that
|
|
@@ -2305,6 +2427,27 @@ function findCallers(index, name, options = {}) {
|
|
|
2305
2427
|
};
|
|
2306
2428
|
}
|
|
2307
2429
|
}
|
|
2430
|
+
// Structural namespace value hop (fix #315, Zod-measured):
|
|
2431
|
+
// `core.globalRegistry.add()` has a module namespace root and
|
|
2432
|
+
// an exported VALUE whose explicit annotation fixes the
|
|
2433
|
+
// receiver type. Follow exact ESM re-export chains (including
|
|
2434
|
+
// barrels) to that annotation; untyped/conflicting/dynamic
|
|
2435
|
+
// surfaces abstain. This is the value-level counterpart of
|
|
2436
|
+
// the declared class-field hop above.
|
|
2437
|
+
if (!fieldHopType && call.isMethod && !call.receiverType &&
|
|
2438
|
+
call.receiverField && !resolvedBySameClass &&
|
|
2439
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
2440
|
+
const moduleValueInfo = {};
|
|
2441
|
+
fieldHopType = _structuralModuleValueFieldType(
|
|
2442
|
+
index, fileEntry, call, moduleValueInfo);
|
|
2443
|
+
if (fieldHopType && moduleValueInfo.fromFile &&
|
|
2444
|
+
!call.receiverTypeFlowFile) {
|
|
2445
|
+
call = {
|
|
2446
|
+
...call,
|
|
2447
|
+
receiverTypeFlowFile: moduleValueInfo.fromFile,
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2308
2451
|
if (!resolvedByExtensionMethod && fileEntry.language === 'csharp' &&
|
|
2309
2452
|
fieldHopType) {
|
|
2310
2453
|
resolvedByExtensionMethod = _csharpExtensionCallMatches(
|
|
@@ -2908,7 +3051,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2908
3051
|
// only when every target is a class method; standalone-function
|
|
2909
3052
|
// and class (constructor) targets keep flowing on import evidence.
|
|
2910
3053
|
if ((!bindingId || recvExportedNamespace) && !resolvedBySameClass && call.isMethod &&
|
|
2911
|
-
(call.receiverIsModule || recvExportedNamespace) &&
|
|
3054
|
+
(call.receiverIsModule || call.receiverModuleSpecifier || recvExportedNamespace) &&
|
|
2912
3055
|
langTraits(fileEntry.language)?.typeSystem === 'structural' &&
|
|
2913
3056
|
targetDefs.length > 0 && targetDefs.every(d => d.className)) {
|
|
2914
3057
|
isUncertain = true;
|
|
@@ -2935,11 +3078,30 @@ function findCallers(index, name, options = {}) {
|
|
|
2935
3078
|
// dynamic CJS surfaces can exceed the modeled ownership);
|
|
2936
3079
|
// unresolved-but-project-looking → visible (resolver gap).
|
|
2937
3080
|
if ((!bindingId || recvExportedNamespace) && !resolvedBySameClass && call.isMethod &&
|
|
2938
|
-
(call.receiverIsModule ||
|
|
3081
|
+
(call.receiverIsModule || call.receiverModuleSpecifier ||
|
|
3082
|
+
recvSubmoduleRel || recvExportedNamespace ||
|
|
3083
|
+
call.receiverModuleComposition) &&
|
|
2939
3084
|
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
2940
|
-
const recvBindings = recvExportedNamespace
|
|
2941
|
-
? [] : _structuralModuleBindings(fileEntry, call);
|
|
2942
3085
|
const tFiles = targetDefinitionFiles;
|
|
3086
|
+
const compositeOwnership = call.receiverModuleComposition
|
|
3087
|
+
? _structuralCompositeModuleOwnership(
|
|
3088
|
+
index, fileEntry, call, tFiles)
|
|
3089
|
+
: null;
|
|
3090
|
+
if (compositeOwnership?.verdict === 'no') {
|
|
3091
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
if (compositeOwnership?.verdict === 'unknown') {
|
|
3095
|
+
if (collectAccount) {
|
|
3096
|
+
routeUnverified(filePath, fileEntry, call,
|
|
3097
|
+
'no-import-link', calledAs);
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
const recvBindings = recvExportedNamespace
|
|
3102
|
+
? [] : (compositeOwnership?.verdict === 'yes'
|
|
3103
|
+
? [compositeOwnership.binding]
|
|
3104
|
+
: _structuralModuleBindings(fileEntry, call));
|
|
2943
3105
|
// Same-file targets get NO bypass (fix #294, flask-measured:
|
|
2944
3106
|
// `import json as _json; _json.dump(...)` in the file
|
|
2945
3107
|
// defining flask's own `dump` confirmed a self-recursive
|
|
@@ -4099,18 +4261,37 @@ function findCallers(index, name, options = {}) {
|
|
|
4099
4261
|
if (r && index.files.has(r)) _modFiles.push(r);
|
|
4100
4262
|
} catch { /* resolver gap — never exclusion evidence */ }
|
|
4101
4263
|
}
|
|
4264
|
+
const _targetFiles = new Set(
|
|
4265
|
+
targetDefs2.map(d => d.file).filter(Boolean));
|
|
4102
4266
|
const _pinnedIn = _modFiles.length > 0 &&
|
|
4103
4267
|
targetDefs2.some(d => _modFiles.includes(d.file));
|
|
4104
|
-
|
|
4268
|
+
// A module/crate surface may re-export the
|
|
4269
|
+
// callable from a child module (`pub use
|
|
4270
|
+
// self::join::join`). Path ownership is
|
|
4271
|
+
// name-aware: reaching a file that declares
|
|
4272
|
+
// a same-named module is not a competing
|
|
4273
|
+
// value-namespace definition. Follow the
|
|
4274
|
+
// exact name through import/re-export
|
|
4275
|
+
// bindings before deciding this path owns a
|
|
4276
|
+
// different callable (fix #302, Rayon).
|
|
4277
|
+
const _reexportVerdicts = _pinnedIn ? [] :
|
|
4278
|
+
_modFiles.map(f => _nameBindingReaches(
|
|
4279
|
+
index, f, name, _targetFiles));
|
|
4280
|
+
const _reexportPinned =
|
|
4281
|
+
_reexportVerdicts.includes('yes');
|
|
4282
|
+
if (!_pinnedIn && !_reexportPinned) {
|
|
4105
4283
|
const _ownsName = _modFiles.some(f => {
|
|
4106
4284
|
const fe2 = index.files.get(f);
|
|
4107
4285
|
return fe2 && fe2.symbols && fe2.symbols.some(s =>
|
|
4108
|
-
s.name === name &&
|
|
4286
|
+
s.name === name && s.type !== 'module' &&
|
|
4287
|
+
!NON_CALLABLE_TYPES.has(s.type));
|
|
4109
4288
|
});
|
|
4110
4289
|
if (_ownsName) {
|
|
4111
4290
|
recordExcluded(filePath, call.line, 'other-definition');
|
|
4112
4291
|
continue;
|
|
4113
4292
|
}
|
|
4293
|
+
// A live but unresolved re-export path
|
|
4294
|
+
// is uncertainty, never negative proof.
|
|
4114
4295
|
routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
|
|
4115
4296
|
dispatchCandidates: methodOwnerKeys().size,
|
|
4116
4297
|
});
|
|
@@ -4398,6 +4579,13 @@ function findCallers(index, name, options = {}) {
|
|
|
4398
4579
|
if (!typeQualifiedReceiver && call.receiverLocalBinding &&
|
|
4399
4580
|
!call.receiverType && !fieldHopType && !fieldDispatchType &&
|
|
4400
4581
|
!call.receiverExternalFlow && !call.receiverQualifiedFlow) {
|
|
4582
|
+
if (call.receiverUntypedIteration) {
|
|
4583
|
+
routeUnverified(filePath, fileEntry, call,
|
|
4584
|
+
'possible-dispatch', calledAs, {
|
|
4585
|
+
dispatchVia: 'untyped loop element',
|
|
4586
|
+
});
|
|
4587
|
+
continue;
|
|
4588
|
+
}
|
|
4401
4589
|
let demoteFlowMap = returnFlowCache.get(filePath);
|
|
4402
4590
|
if (demoteFlowMap === undefined) {
|
|
4403
4591
|
demoteFlowMap = _buildReturnTypeFlowMap(index, filePath, calls);
|
|
@@ -4446,7 +4634,7 @@ function findCallers(index, name, options = {}) {
|
|
|
4446
4634
|
// root counts only when the resolver PROVED it a
|
|
4447
4635
|
// submodule (#224 — a from-import name may be a
|
|
4448
4636
|
// plain symbol, never assume).
|
|
4449
|
-
if (!typeQualifiedReceiver && !knownDispatchType &&
|
|
4637
|
+
if (!call.moduleOwnedPath && !typeQualifiedReceiver && !knownDispatchType &&
|
|
4450
4638
|
call.receiverRoot && !call.receiverRootType &&
|
|
4451
4639
|
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
4452
4640
|
const rootBinding = (fileEntry.importBindings || []).find(b =>
|
|
@@ -5114,6 +5302,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
5114
5302
|
// Return-type flow map (lazy — only built if a single-owner
|
|
5115
5303
|
// resolution needs the external-producer/typed-receiver defeater).
|
|
5116
5304
|
let _flowMap;
|
|
5305
|
+
const pythonIndexedReceiverCache = new Map();
|
|
5117
5306
|
const flowMap = () => {
|
|
5118
5307
|
if (_flowMap === undefined) {
|
|
5119
5308
|
if (queryProfile) {
|
|
@@ -5131,7 +5320,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
5131
5320
|
const mayNeedDirectReceiverFlow = call =>
|
|
5132
5321
|
call.isMethod && call.receiver && !call.receiverType &&
|
|
5133
5322
|
!call.receiverPatternShadow &&
|
|
5134
|
-
!
|
|
5323
|
+
!_isReservedReceiver(language, call.receiver) &&
|
|
5135
5324
|
!call.isPathCall && !call.receiverIsModule &&
|
|
5136
5325
|
!call.receiverIsChainRoot;
|
|
5137
5326
|
// Chained-receiver fold context (fix #268 — the #258 rails, callee
|
|
@@ -5140,6 +5329,11 @@ function findCallees(index, definition, options = {}) {
|
|
|
5140
5329
|
const foldCtx = () => {
|
|
5141
5330
|
if (!calleeFoldCtx) {
|
|
5142
5331
|
calleeFoldCtx = { memo: new Map(), visiting: new Set(), records: calls,
|
|
5332
|
+
// A called local arrow factory may be declared outside
|
|
5333
|
+
// the current callee definition. Keep the narrow records
|
|
5334
|
+
// set for ordinary producer indexing, but expose the
|
|
5335
|
+
// already-loaded file records for exact returned spans.
|
|
5336
|
+
allRecords: allCalls,
|
|
5143
5337
|
getFlowMap: () => flowMap() };
|
|
5144
5338
|
}
|
|
5145
5339
|
return calleeFoldCtx;
|
|
@@ -5149,6 +5343,25 @@ function findCallees(index, definition, options = {}) {
|
|
|
5149
5343
|
for (let call of calls) {
|
|
5150
5344
|
siteOrdinal++;
|
|
5151
5345
|
const siteId = siteOrdinal;
|
|
5346
|
+
if (language === 'go' && call.isMethod &&
|
|
5347
|
+
!call.receiverType && call.receiverIndexField) {
|
|
5348
|
+
const indexedType = _goIndexedReceiverType(index, def.file, call);
|
|
5349
|
+
if (indexedType?.type) {
|
|
5350
|
+
call = {
|
|
5351
|
+
...call,
|
|
5352
|
+
receiverType: indexedType.type,
|
|
5353
|
+
...(indexedType.fromFile && {
|
|
5354
|
+
receiverTypeFlowFile: indexedType.fromFile,
|
|
5355
|
+
}),
|
|
5356
|
+
};
|
|
5357
|
+
} else if (indexedType?.externalVia) {
|
|
5358
|
+
call = {
|
|
5359
|
+
...call,
|
|
5360
|
+
receiverExternalFlow: indexedType.externalVia,
|
|
5361
|
+
receiverExternalConcreteFlow: true,
|
|
5362
|
+
};
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5152
5365
|
// Filter to calls within this function's scope
|
|
5153
5366
|
// Method 1: Direct match via enclosingFunction (fast path for direct calls)
|
|
5154
5367
|
const isDirectMatch = call.enclosingFunction &&
|
|
@@ -5264,6 +5477,25 @@ function findCallees(index, definition, options = {}) {
|
|
|
5264
5477
|
}
|
|
5265
5478
|
}
|
|
5266
5479
|
|
|
5480
|
+
// Python indexed receivers share the caller-side #324 contract:
|
|
5481
|
+
// only an exact project container type plus its declared
|
|
5482
|
+
// `__getitem__` return type may identify the selected value.
|
|
5483
|
+
if (language === 'python' && call.isMethod &&
|
|
5484
|
+
!call.receiverType && call.receiverSubscriptRoot) {
|
|
5485
|
+
const indexedType = _pythonIndexedReceiverType(
|
|
5486
|
+
index, def.file, call, flowMap,
|
|
5487
|
+
pythonIndexedReceiverCache);
|
|
5488
|
+
if (indexedType?.type) {
|
|
5489
|
+
call = {
|
|
5490
|
+
...call,
|
|
5491
|
+
receiverType: indexedType.type,
|
|
5492
|
+
...(indexedType.fromFile && {
|
|
5493
|
+
receiverTypeFlowFile: indexedType.fromFile,
|
|
5494
|
+
}),
|
|
5495
|
+
};
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5267
5499
|
// Query-time return flow for an ordinary receiver assignment:
|
|
5268
5500
|
// `v := New(); v.ReadConfig()`. The compiler-declared return type
|
|
5269
5501
|
// is stronger than constructor-name guesses and must participate
|
|
@@ -5271,6 +5503,17 @@ function findCallees(index, definition, options = {}) {
|
|
|
5271
5503
|
const directReceiverFlow = mayNeedDirectReceiverFlow(call)
|
|
5272
5504
|
? _lookupReturnTypeFlow(flowMap(), call)
|
|
5273
5505
|
: undefined;
|
|
5506
|
+
// Caller-side fix #305 twin: a loop element drawn from an
|
|
5507
|
+
// untyped identifier/attribute iterable has no class identity.
|
|
5508
|
+
// Unique project ownership of the method spelling is not enough
|
|
5509
|
+
// to invent an exact callee.
|
|
5510
|
+
if (collectAccount && call.isMethod && !call.receiverType &&
|
|
5511
|
+
call.receiverUntypedIteration) {
|
|
5512
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
5513
|
+
dispatchVia: 'untyped loop element',
|
|
5514
|
+
});
|
|
5515
|
+
continue;
|
|
5516
|
+
}
|
|
5274
5517
|
// Callee-side twin of the structural caller gate (#222(4)). A
|
|
5275
5518
|
// local receiver whose nearest producer was examined but could
|
|
5276
5519
|
// not be typed (`C2 = decorator(Base); value = C2()`) has unknown
|
|
@@ -5303,19 +5546,26 @@ function findCallees(index, definition, options = {}) {
|
|
|
5303
5546
|
hopRoot = localTypes.get(call.receiverRoot);
|
|
5304
5547
|
}
|
|
5305
5548
|
if (!hopRoot && call.receiverRoot &&
|
|
5306
|
-
!
|
|
5549
|
+
!_isReservedReceiver(language, call.receiverRoot)) {
|
|
5307
5550
|
const inferredRoot = _lookupReturnTypeFlow(flowMap(), {
|
|
5308
5551
|
...call,
|
|
5309
5552
|
receiver: call.receiverRoot,
|
|
5310
5553
|
});
|
|
5311
|
-
if (inferredRoot?.type)
|
|
5554
|
+
if (inferredRoot?.type) {
|
|
5555
|
+
hopRoot = inferredRoot.type;
|
|
5556
|
+
} else if (inferredRoot?.externalVia) {
|
|
5557
|
+
fieldHopInfo = {
|
|
5558
|
+
externalVia:
|
|
5559
|
+
`${inferredRoot.externalVia}.${call.receiverField}`,
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5312
5562
|
}
|
|
5313
5563
|
if (!hopRoot && call.receiverRoot === 'this' &&
|
|
5314
5564
|
langTraits(language)?.typeSystem === 'structural') {
|
|
5315
5565
|
hopRoot = index.findEnclosingFunction(def.file, call.line, true)?.className;
|
|
5316
5566
|
}
|
|
5317
5567
|
if (hopRoot) {
|
|
5318
|
-
fieldHopInfo = {};
|
|
5568
|
+
fieldHopInfo = fieldHopInfo || {};
|
|
5319
5569
|
const fields = call.receiverFields || [call.receiverField];
|
|
5320
5570
|
fieldHopType = _declaredFieldPathType(index, hopRoot, fields,
|
|
5321
5571
|
language, fieldHopInfo, call.receiverRootNamespace);
|
|
@@ -5349,6 +5599,12 @@ function findCallees(index, definition, options = {}) {
|
|
|
5349
5599
|
}
|
|
5350
5600
|
}
|
|
5351
5601
|
}
|
|
5602
|
+
if (!fieldHopType &&
|
|
5603
|
+
langTraits(language)?.typeSystem === 'structural') {
|
|
5604
|
+
fieldHopInfo = fieldHopInfo || {};
|
|
5605
|
+
fieldHopType = _structuralModuleValueFieldType(
|
|
5606
|
+
index, fileEntry, call, fieldHopInfo);
|
|
5607
|
+
}
|
|
5352
5608
|
}
|
|
5353
5609
|
|
|
5354
5610
|
if (fieldDispatchType) {
|
|
@@ -5510,7 +5766,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
5510
5766
|
if (call.isMethod && !call.isConstructor && call.receiver &&
|
|
5511
5767
|
!call.receiverType && !fieldHopType && !goImportModule &&
|
|
5512
5768
|
!call.receiverIsModule && !call.selfAttribute &&
|
|
5513
|
-
!
|
|
5769
|
+
!_isReservedReceiver(language, call.receiver) &&
|
|
5514
5770
|
!(localTypes && localTypes.has(call.receiver))) {
|
|
5515
5771
|
typeQual = _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language);
|
|
5516
5772
|
}
|
|
@@ -5551,7 +5807,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
5551
5807
|
(call.receiver === 'Self' && language === 'rust')) {
|
|
5552
5808
|
// self.method() / cls.method() / this.method() — resolve to same-class method below
|
|
5553
5809
|
// Rust Self::method() resolves same-impl the same way (fix #236, the #232 callee analog)
|
|
5554
|
-
} else if (call.receiver === 'super' ||
|
|
5810
|
+
} else if (call.receiver === 'super' ||
|
|
5811
|
+
(call.receiver === 'base' && language === 'csharp')) {
|
|
5555
5812
|
// super().method() — resolve to parent class method below
|
|
5556
5813
|
} else if (directReceiverFlow?.externalVia) {
|
|
5557
5814
|
if (directReceiverFlow.externalConcrete) {
|
|
@@ -5682,7 +5939,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
5682
5939
|
const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
5683
5940
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
5684
5941
|
// Same-class overload selection by static call shape (fix #268)
|
|
5685
|
-
const receiverOriginFile =
|
|
5942
|
+
const receiverOriginFile = call.receiverTypeFlowFile ||
|
|
5943
|
+
directReceiverFlow?.fromFile ||
|
|
5686
5944
|
fieldHopInfo?.fromFile ||
|
|
5687
5945
|
(call.receiverType
|
|
5688
5946
|
? _resolveFlowTypeOrigin(
|
|
@@ -5696,7 +5954,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
5696
5954
|
(symbol.file &&
|
|
5697
5955
|
path.dirname(symbol.file) === qualifiedType.dir)) &&
|
|
5698
5956
|
(!receiverOriginFile || !symbol.file ||
|
|
5699
|
-
((language === '
|
|
5957
|
+
((langTraits(language)?.typeSystem === 'structural' ||
|
|
5958
|
+
language === 'java' || language === 'csharp')
|
|
5700
5959
|
? symbol.file === receiverOriginFile
|
|
5701
5960
|
: path.dirname(symbol.file) ===
|
|
5702
5961
|
path.dirname(receiverOriginFile)));
|
|
@@ -5786,18 +6045,20 @@ function findCallees(index, definition, options = {}) {
|
|
|
5786
6045
|
continue;
|
|
5787
6046
|
}
|
|
5788
6047
|
// Structural legacy mode retains its historical fallback.
|
|
5789
|
-
} else if (call.receiverCall && (!call.receiver || call.receiverIsChainRoot) &&
|
|
5790
|
-
|
|
5791
|
-
// Chained receiver,
|
|
5792
|
-
//
|
|
5793
|
-
// `
|
|
5794
|
-
//
|
|
5795
|
-
//
|
|
5796
|
-
//
|
|
5797
|
-
//
|
|
5798
|
-
//
|
|
6048
|
+
} else if (call.receiverCall && (!call.receiver || call.receiverIsChainRoot) &&
|
|
6049
|
+
!call.receiverIsModule && !call.isConstructor) {
|
|
6050
|
+
// Chained receiver, callee direction (fix #302):
|
|
6051
|
+
// `Environment().getattr(...)` is the structural twin of
|
|
6052
|
+
// the nominal #268 shapes (`m.NotFound().ServeHTTP(...)`).
|
|
6053
|
+
// The shared fold already resolves constructor roots and
|
|
6054
|
+
// method-return chains for both type-system families; the
|
|
6055
|
+
// old nominal-only gate threw that evidence away and
|
|
6056
|
+
// classified exact Python/JS calls as external. Module
|
|
6057
|
+
// producers (`require("./lib").target()`) stay on the
|
|
6058
|
+
// stronger name-aware module route below.
|
|
5799
6059
|
let chained = _foldChainedReceiverType(index, fileEntry, def.file, call, foldCtx());
|
|
5800
|
-
if (!chained || (!chained.type && !chained.externalVia))
|
|
6060
|
+
if ((!chained || (!chained.type && !chained.externalVia)) &&
|
|
6061
|
+
langTraits(language)?.typeSystem === 'nominal') {
|
|
5801
6062
|
chained = _nominalChainedReceiverType(index, call, fileEntry, def.file);
|
|
5802
6063
|
}
|
|
5803
6064
|
if (chained?.type) {
|
|
@@ -5816,7 +6077,12 @@ function findCallees(index, definition, options = {}) {
|
|
|
5816
6077
|
def.file, acceptsChainedDefinition);
|
|
5817
6078
|
if (sel.match) {
|
|
5818
6079
|
const match = sel.match;
|
|
5819
|
-
|
|
6080
|
+
// A direct constructor expression fixes the
|
|
6081
|
+
// runtime class exactly (`Environment()` cannot be
|
|
6082
|
+
// a subclass instance). Declared/returned base
|
|
6083
|
+
// types keep normal virtual-dispatch demotion.
|
|
6084
|
+
if (!chained.exactConstructor &&
|
|
6085
|
+
routeVirtualOverride(siteId, call, chained.type, match)) continue;
|
|
5820
6086
|
const key = match.bindingId || `${chained.type}.${call.name}`;
|
|
5821
6087
|
const existing = callees.get(key);
|
|
5822
6088
|
if (existing) {
|
|
@@ -5906,7 +6172,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
5906
6172
|
}
|
|
5907
6173
|
if (collectAccount && call.receiver && !call.receiverIsModule && !call.receiverType &&
|
|
5908
6174
|
!call.receiverCall && !call.isPotentialCallback &&
|
|
5909
|
-
!
|
|
6175
|
+
!_isReservedReceiver(language, call.receiver)) {
|
|
5910
6176
|
const owners = new Set((index.symbols.get(call.name) || [])
|
|
5911
6177
|
.filter(s => !NON_CALLABLE_TYPES.has(s.type))
|
|
5912
6178
|
.map(s => s.className || (s.receiver && s.receiver.replace(/^\*/, '')))
|
|
@@ -5926,7 +6192,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
5926
6192
|
// `super(...)` 'constructor' record were routed external here
|
|
5927
6193
|
// because __init__/constructor sit in the builtin name sets).
|
|
5928
6194
|
const selfShaped = call.isMethod &&
|
|
5929
|
-
(['self', 'cls', 'this', 'super'
|
|
6195
|
+
(['self', 'cls', 'this', 'super'].includes(call.receiver) ||
|
|
6196
|
+
(call.receiver === 'base' && language === 'csharp') ||
|
|
5930
6197
|
(call.receiver === 'Self' && language === 'rust'));
|
|
5931
6198
|
// Builtin/global names are shadowable. `Request` is a web global,
|
|
5932
6199
|
// but `const Request = require('./request')` owns `new Request()`
|
|
@@ -6011,7 +6278,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
6011
6278
|
|
|
6012
6279
|
// Collect super().method() calls for parent-class resolution
|
|
6013
6280
|
if (call.isMethod &&
|
|
6014
|
-
(call.receiver === 'super' ||
|
|
6281
|
+
(call.receiver === 'super' ||
|
|
6282
|
+
(call.receiver === 'base' && language === 'csharp'))) {
|
|
6015
6283
|
if (!selfMethodCalls) selfMethodCalls = [];
|
|
6016
6284
|
selfMethodCalls.push({ call, siteId });
|
|
6017
6285
|
continue;
|
|
@@ -6024,7 +6292,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
6024
6292
|
// the callee. Follow the module's name-level re-export chain and
|
|
6025
6293
|
// add only definitions it actually exposes. Unknown CJS/dynamic
|
|
6026
6294
|
// surfaces stay visible; external modules are external.
|
|
6027
|
-
if (call.isMethod && call.receiverIsModule
|
|
6295
|
+
if (call.isMethod && (call.receiverIsModule ||
|
|
6296
|
+
call.receiverModuleSpecifier || call.receiverModuleComposition) &&
|
|
6028
6297
|
langTraits(language)?.typeSystem === 'structural') {
|
|
6029
6298
|
const moduleRoute = _calleeStructuralModuleRoute(index, fileEntry, call, language);
|
|
6030
6299
|
if (moduleRoute.matches?.length) {
|
|
@@ -6147,7 +6416,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
6147
6416
|
// also named ReadConfig. Otherwise the bare wrapper steals the
|
|
6148
6417
|
// exact callee before receiver evidence is considered.
|
|
6149
6418
|
const receiverBlindMethodBinding = call.isMethod &&
|
|
6150
|
-
!
|
|
6419
|
+
!_isReservedReceiver(language, call.receiver);
|
|
6151
6420
|
let bindings = receiverBlindMethodBinding ? [] :
|
|
6152
6421
|
fileEntry.bindings.filter(b => b.name === call.name);
|
|
6153
6422
|
// For Go, also check sibling files in same directory (same
|
|
@@ -6682,8 +6951,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
6682
6951
|
}
|
|
6683
6952
|
|
|
6684
6953
|
// For super().method(), skip same-class — start from parent
|
|
6685
|
-
const parentOnlyReceiver =
|
|
6686
|
-
call.receiver === '
|
|
6954
|
+
const parentOnlyReceiver = call.receiver === 'super' ||
|
|
6955
|
+
(call.receiver === 'base' && language === 'csharp');
|
|
6687
6956
|
const selectOwner = owner => _calleeOverloadSelect(
|
|
6688
6957
|
index,
|
|
6689
6958
|
call,
|
|
@@ -7031,12 +7300,33 @@ function getInstanceAttributeTypes(index, filePath, className) {
|
|
|
7031
7300
|
const parser = getParser('python');
|
|
7032
7301
|
const fileEntry = index.files.get(filePath);
|
|
7033
7302
|
fileCache = langModule.findInstanceAttributeTypes(content, parser, {
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7303
|
+
resolveTypeAliasMembers(typeName) {
|
|
7304
|
+
const owner = _resolveFlowTypeOrigin(
|
|
7305
|
+
index, filePath, typeName);
|
|
7306
|
+
if (!owner?.fromFile) return null;
|
|
7307
|
+
const definitions = (index.symbols.get(typeName) || [])
|
|
7308
|
+
.filter(definition => definition.file === owner.fromFile &&
|
|
7309
|
+
definition.type === 'type' &&
|
|
7310
|
+
Array.isArray(definition.aliasMembers));
|
|
7311
|
+
if (definitions.length === 0) return null;
|
|
7312
|
+
const identities = new Set(definitions.map(definition =>
|
|
7313
|
+
definition.aliasMembers.join('\0')));
|
|
7314
|
+
return identities.size === 1
|
|
7315
|
+
? [...definitions[0].aliasMembers] : null;
|
|
7316
|
+
},
|
|
7317
|
+
resolveCallType(moduleName, functionName) {
|
|
7318
|
+
if (_pythonBuiltinContractAllowed(index, fileEntry, moduleName)) {
|
|
7319
|
+
const builtin = langModule.getBuiltinCallReturnType?.(
|
|
7320
|
+
moduleName, functionName);
|
|
7321
|
+
if (builtin) return builtin;
|
|
7322
|
+
}
|
|
7323
|
+
const owner = _resolveFlowTypeOrigin(
|
|
7324
|
+
index, filePath, moduleName);
|
|
7325
|
+
if (!owner?.fromFile) return null;
|
|
7326
|
+
const result = _methodReturnOnType(
|
|
7327
|
+
index, moduleName, owner.fromFile, functionName,
|
|
7328
|
+
'python', { filePath, consumerAwaited: false });
|
|
7329
|
+
return result?.fromFile ? result.type : null;
|
|
7040
7330
|
},
|
|
7041
7331
|
});
|
|
7042
7332
|
index._attrTypeCache.set(filePath, fileCache);
|
|
@@ -7141,11 +7431,11 @@ function _typeNameFromReturnAnnotation(text) {
|
|
|
7141
7431
|
t = m[2].trim();
|
|
7142
7432
|
}
|
|
7143
7433
|
// generic base: Foo[...] / Foo<...> → Foo (the value is a Foo)
|
|
7144
|
-
m = t.match(/^([\w
|
|
7434
|
+
m = t.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*[[<]/);
|
|
7145
7435
|
if (m) t = m[1];
|
|
7146
7436
|
// dotted → last segment; validate a bare identifier remains
|
|
7147
7437
|
const last = t.split('.').pop();
|
|
7148
|
-
return /^[A-Za-z_]\w*$/.test(last) ? last : undefined;
|
|
7438
|
+
return /^[A-Za-z_$][\w$]*$/.test(last) ? last : undefined;
|
|
7149
7439
|
}
|
|
7150
7440
|
|
|
7151
7441
|
/**
|
|
@@ -7186,6 +7476,19 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7186
7476
|
// Map.has() so files without resolvable flow are cheap too.
|
|
7187
7477
|
const opCache = index._opReturnTypeFlowCache;
|
|
7188
7478
|
if (opCache?.has(filePath)) return opCache.get(filePath);
|
|
7479
|
+
// Return-flow derives from symbols in OTHER files, so it must never enter
|
|
7480
|
+
// the persisted calls cache. It is nevertheless immutable until the next
|
|
7481
|
+
// ProjectIndex build. Retain a bounded, calls-array-identity-guarded copy
|
|
7482
|
+
// across composed agent queries; build() clears it when any annotation or
|
|
7483
|
+
// import graph could have changed.
|
|
7484
|
+
const persistentCache = index._returnTypeFlowCache;
|
|
7485
|
+
const persistent = persistentCache?.get(filePath);
|
|
7486
|
+
if (persistent?.calls === calls) {
|
|
7487
|
+
persistentCache.delete(filePath);
|
|
7488
|
+
persistentCache.set(filePath, persistent);
|
|
7489
|
+
if (opCache) opCache.set(filePath, persistent.map);
|
|
7490
|
+
return persistent.map;
|
|
7491
|
+
}
|
|
7189
7492
|
const fileEntry = index.files.get(filePath);
|
|
7190
7493
|
const language = fileEntry?.language;
|
|
7191
7494
|
const nominal = langTraits(language)?.typeSystem === 'nominal';
|
|
@@ -7247,6 +7550,23 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7247
7550
|
if (via) routeUnknownAssignment(call, via);
|
|
7248
7551
|
continue;
|
|
7249
7552
|
}
|
|
7553
|
+
// Package-owned value method producer (fix #306, chi-measured):
|
|
7554
|
+
// `resp, err := http.DefaultClient.Do(req)`. The parser correctly
|
|
7555
|
+
// retains `http` as a module root and `DefaultClient` as its exported
|
|
7556
|
+
// value, but this shape is neither an ordinary package call nor a
|
|
7557
|
+
// locally typed method receiver. When that package is external, its
|
|
7558
|
+
// result is externally decided and must defeat later single-owner
|
|
7559
|
+
// confirmation (`resp.Body.Close()`). Project-owned package values
|
|
7560
|
+
// abstain here until their field declarations can be resolved.
|
|
7561
|
+
if (language === 'go' && call.isMethod && call.receiverRootIsModule &&
|
|
7562
|
+
call.receiverRoot && call.receiverField) {
|
|
7563
|
+
const qualified = _goQualifiedReceiverType(
|
|
7564
|
+
index, fileEntry, call.receiverRoot, call.receiverField);
|
|
7565
|
+
if (qualified && qualified.kind !== 'project') {
|
|
7566
|
+
routeUnknownAssignment(call, `${qualified.via}.${call.name}`);
|
|
7567
|
+
continue;
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7250
7570
|
const delegatedUnwrapAssignment = language === 'rust' &&
|
|
7251
7571
|
call.receiverCall && calls.some(candidate =>
|
|
7252
7572
|
candidate !== call &&
|
|
@@ -7280,11 +7600,16 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7280
7600
|
continue;
|
|
7281
7601
|
}
|
|
7282
7602
|
|
|
7283
|
-
// Assigned builder chain (chi-measured):
|
|
7284
|
-
// `hr := RouteHeaders().Route(...)`
|
|
7285
|
-
// as the assignment producer. Fold
|
|
7286
|
-
// before composing later
|
|
7287
|
-
|
|
7603
|
+
// Assigned builder chain (chi/zod-measured):
|
|
7604
|
+
// `hr := RouteHeaders().Route(...)` / `const s = z.array().refine(...)`
|
|
7605
|
+
// stores the OUTERMOST method call as the assignment producer. Fold
|
|
7606
|
+
// the chain to its declared result before composing a later receiver.
|
|
7607
|
+
// Macro-result folding remains nominal-only; structural call chains
|
|
7608
|
+
// use the same compiler-annotation and module-ownership rails as the
|
|
7609
|
+
// already-supported immediate chained-receiver path.
|
|
7610
|
+
if (call.receiverCall ||
|
|
7611
|
+
(!nominal && call.isMethod && call.receiverRoot && call.receiverField) ||
|
|
7612
|
+
(nominal && call.isMacro)) {
|
|
7288
7613
|
let folded = _typeOfCallResultFold(
|
|
7289
7614
|
index, fileEntry, filePath, call, assignedFoldCtx);
|
|
7290
7615
|
// Rust result aliases are commonly imported under a local name
|
|
@@ -7365,7 +7690,8 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7365
7690
|
} else if (callableFlow?.returnedFunctionResult) {
|
|
7366
7691
|
returnType = callableFlow.returnedFunctionResult;
|
|
7367
7692
|
fromFile = callableFlow.fromFile;
|
|
7368
|
-
} else if (call.isMethod && call.receiverType
|
|
7693
|
+
} else if (call.isMethod && call.receiverType &&
|
|
7694
|
+
!call.receiverTypeGuessed) {
|
|
7369
7695
|
const defs = index.symbols.get(call.name) || [];
|
|
7370
7696
|
if (nominal) {
|
|
7371
7697
|
const matches = defs.filter(d => d.className === call.receiverType && d.returnType);
|
|
@@ -7402,6 +7728,25 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7402
7728
|
index._getInheritanceParents?.(owner, filePath) || []))];
|
|
7403
7729
|
}
|
|
7404
7730
|
}
|
|
7731
|
+
} else if (!nominal && language === 'python' && call.isMethod &&
|
|
7732
|
+
call.receiver && !call.receiverLocalBinding &&
|
|
7733
|
+
!call.receiverIsModule && !call.receiverModuleComposition) {
|
|
7734
|
+
// Exact imported class factory: `text = Text.from_markup(...)`.
|
|
7735
|
+
// The parser intentionally does not label every capitalized
|
|
7736
|
+
// receiver as an instance type. Pin the value to an indexed type
|
|
7737
|
+
// in the caller's import scope first, then reuse the method
|
|
7738
|
+
// contract agreement discipline to type the assigned result.
|
|
7739
|
+
const owner = _resolveFlowTypeOrigin(index, filePath, call.receiver);
|
|
7740
|
+
const resolved = owner?.fromFile
|
|
7741
|
+
? _methodReturnOnType(
|
|
7742
|
+
index, call.receiver, owner.fromFile, call.name,
|
|
7743
|
+
language, { filePath, consumerAwaited: false })
|
|
7744
|
+
: null;
|
|
7745
|
+
if (resolved?.type) {
|
|
7746
|
+
returnType = resolved.type;
|
|
7747
|
+
fromFile = resolved.fromFile;
|
|
7748
|
+
selfClass = call.receiver;
|
|
7749
|
+
}
|
|
7405
7750
|
} else if (call.isMethod && call.receiver &&
|
|
7406
7751
|
!['self', 'this', 'cls'].includes(call.receiver) &&
|
|
7407
7752
|
_lookupReturnTypeFlow(map, call)) {
|
|
@@ -7583,7 +7928,9 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7583
7928
|
}
|
|
7584
7929
|
continue;
|
|
7585
7930
|
}
|
|
7586
|
-
} else if (!nominal && call.isMethod &&
|
|
7931
|
+
} else if (!nominal && call.isMethod &&
|
|
7932
|
+
(call.receiverIsModule || call.receiverModuleSpecifier ||
|
|
7933
|
+
call.receiverModuleComposition) &&
|
|
7587
7934
|
(call.receiver || call.receiverModuleSpecifier)) {
|
|
7588
7935
|
// Structural module-qualified producer (fix #209): schema =
|
|
7589
7936
|
// z.string() — the module alias resolves through the file's
|
|
@@ -7591,8 +7938,15 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7591
7938
|
// and the producer's return annotation types the variable.
|
|
7592
7939
|
// Standalone exports only (className-less): a module attr is
|
|
7593
7940
|
// never a class method.
|
|
7594
|
-
const
|
|
7595
|
-
|
|
7941
|
+
const composite = call.receiverModuleComposition
|
|
7942
|
+
? _structuralCompositeModuleOwnership(index, fileEntry, call)
|
|
7943
|
+
: null;
|
|
7944
|
+
if (composite && composite.verdict !== 'yes') continue;
|
|
7945
|
+
const binding = composite?.binding ||
|
|
7946
|
+
_structuralModuleBindings(fileEntry, call)[0];
|
|
7947
|
+
const rel = composite?.rel ||
|
|
7948
|
+
(binding && fileEntry.moduleResolved &&
|
|
7949
|
+
fileEntry.moduleResolved[binding.module]);
|
|
7596
7950
|
if (binding && !rel) {
|
|
7597
7951
|
// External module producer (fix #222, httpx-measured — the
|
|
7598
7952
|
// #220 Go external-producer rule for structural languages):
|
|
@@ -7620,6 +7974,19 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7620
7974
|
const cands = (index.symbols.get(call.name) || [])
|
|
7621
7975
|
.filter(d => !NON_CALLABLE_TYPES.has(d.type) && d.returnType && !d.className);
|
|
7622
7976
|
let matches = cands.filter(d => d.file === modFile);
|
|
7977
|
+
// A namespace binding owns a NAME through the complete
|
|
7978
|
+
// re-export surface, not merely through one import edge.
|
|
7979
|
+
// Zod's self exports run index -> external -> types; the
|
|
7980
|
+
// one-hop fallback below lost the producer's defining-file
|
|
7981
|
+
// provenance and conflated the v3/v4 types with identical
|
|
7982
|
+
// short names. Reuse the same name-aware ownership proof as
|
|
7983
|
+
// chained producers before retaining the legacy fallback.
|
|
7984
|
+
if (matches.length === 0) {
|
|
7985
|
+
matches = cands.filter(definition =>
|
|
7986
|
+
_importedNamespaceMemberOwnership(
|
|
7987
|
+
index, fileEntry, call,
|
|
7988
|
+
new Set([definition.file]))?.verdict === 'yes');
|
|
7989
|
+
}
|
|
7623
7990
|
if (matches.length === 0) {
|
|
7624
7991
|
const hop = index.importGraph.get(modFile);
|
|
7625
7992
|
if (hop) matches = cands.filter(d => hop.has(d.file));
|
|
@@ -7974,6 +8341,13 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
7974
8341
|
}) });
|
|
7975
8342
|
}
|
|
7976
8343
|
if (opCache) opCache.set(filePath, map);
|
|
8344
|
+
if (persistentCache) {
|
|
8345
|
+
persistentCache.delete(filePath);
|
|
8346
|
+
persistentCache.set(filePath, { calls, map });
|
|
8347
|
+
if (persistentCache.size > CROSS_OPERATION_FLOW_CACHE_LIMIT) {
|
|
8348
|
+
persistentCache.delete(persistentCache.keys().next().value);
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
7977
8351
|
return map;
|
|
7978
8352
|
}
|
|
7979
8353
|
|
|
@@ -8187,7 +8561,6 @@ function _returnTypeNameNominal(text, language, opts = {}) {
|
|
|
8187
8561
|
if (t.startsWith('(')) {
|
|
8188
8562
|
if (!opts.tuple) return undefined;
|
|
8189
8563
|
const inner = t.slice(1, -1);
|
|
8190
|
-
if (inner.includes('func(') || inner.includes('func (')) return undefined;
|
|
8191
8564
|
const position = Number.isInteger(opts.tupleIndex)
|
|
8192
8565
|
? opts.tupleIndex : 0;
|
|
8193
8566
|
const item = _splitTopLevelGenericArgs(inner)[position]?.trim();
|
|
@@ -8850,13 +9223,41 @@ function _receiverPackageResolution(index, fileEntry, receiver, targetDefs) {
|
|
|
8850
9223
|
* back to the original route through records this chase follows or flags).
|
|
8851
9224
|
*/
|
|
8852
9225
|
function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
9226
|
+
// The same export path is asked once per competing definition and again
|
|
9227
|
+
// by caller/callee projections in a composed command. The graph and file
|
|
9228
|
+
// surfaces are immutable during an operation, so retain the tri-state
|
|
9229
|
+
// verdict beside the existing import-reachability memo. Target identity
|
|
9230
|
+
// is part of the key; no answer can leak between pinned definitions.
|
|
9231
|
+
const opCache = index._opImportReachCache;
|
|
9232
|
+
const targetKey = [...targetFiles].sort(codeUnitCompare).join('\x00');
|
|
9233
|
+
const cacheKey = `name\x00${maxDepth}\x00${startAbs}\x00${name}\x00${targetKey}`;
|
|
9234
|
+
if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
|
|
9235
|
+
const persistentCache = index._nameBindingReachCache;
|
|
9236
|
+
if (persistentCache?.has(cacheKey)) {
|
|
9237
|
+
const value = persistentCache.get(cacheKey);
|
|
9238
|
+
// Map insertion order is the LRU order.
|
|
9239
|
+
persistentCache.delete(cacheKey);
|
|
9240
|
+
persistentCache.set(cacheKey, value);
|
|
9241
|
+
if (opCache) opCache.set(cacheKey, value);
|
|
9242
|
+
return value;
|
|
9243
|
+
}
|
|
9244
|
+
const finish = value => {
|
|
9245
|
+
if (opCache) opCache.set(cacheKey, value);
|
|
9246
|
+
if (persistentCache) {
|
|
9247
|
+
persistentCache.set(cacheKey, value);
|
|
9248
|
+
if (persistentCache.size > 16384) {
|
|
9249
|
+
persistentCache.delete(persistentCache.keys().next().value);
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
return value;
|
|
9253
|
+
};
|
|
8853
9254
|
let unknown = false;
|
|
8854
9255
|
const visited = new Set();
|
|
8855
9256
|
let frontier = [[startAbs, name]];
|
|
8856
9257
|
for (let d = 0; d <= maxDepth && frontier.length > 0; d++) {
|
|
8857
9258
|
const next = [];
|
|
8858
9259
|
for (const [abs, attr] of frontier) {
|
|
8859
|
-
if (targetFiles.has(abs)) return 'yes';
|
|
9260
|
+
if (targetFiles.has(abs)) return finish('yes');
|
|
8860
9261
|
const stateKey = `${abs}\x00${attr}`;
|
|
8861
9262
|
if (visited.has(stateKey)) continue;
|
|
8862
9263
|
visited.add(stateKey);
|
|
@@ -8886,7 +9287,7 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
8886
9287
|
e.defaultLike && e.type === 'module.exports' &&
|
|
8887
9288
|
(e.alias || e.name) !== attr);
|
|
8888
9289
|
if (staticOwner && !competingDynamic) {
|
|
8889
|
-
return 'no';
|
|
9290
|
+
return finish('no');
|
|
8890
9291
|
}
|
|
8891
9292
|
|
|
8892
9293
|
const enqueue = (module, nextAttr) => {
|
|
@@ -8944,7 +9345,7 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
8944
9345
|
frontier = next;
|
|
8945
9346
|
}
|
|
8946
9347
|
if (frontier.length > 0) unknown = true; // depth exhausted with live paths
|
|
8947
|
-
return unknown ? 'unknown' : 'no';
|
|
9348
|
+
return finish(unknown ? 'unknown' : 'no');
|
|
8948
9349
|
}
|
|
8949
9350
|
|
|
8950
9351
|
/**
|
|
@@ -9143,6 +9544,122 @@ function _importedNamespaceMemberOwnership(index, fileEntry, call, targetFiles)
|
|
|
9143
9544
|
return { verdict: unknown ? 'unknown' : 'no' };
|
|
9144
9545
|
}
|
|
9145
9546
|
|
|
9547
|
+
/**
|
|
9548
|
+
* Resolve the compiler-declared type of an exported JS/TS value through an
|
|
9549
|
+
* exact ESM export chain. This is deliberately narrower than ordinary name
|
|
9550
|
+
* ownership: a local value without an explicit annotation, a dynamic/CJS
|
|
9551
|
+
* surface, an unresolved module, or competing exports returns unknown.
|
|
9552
|
+
*/
|
|
9553
|
+
function _structuralExportedValueType(
|
|
9554
|
+
index, startAbs, exposedName, language, maxDepth = 6, visited = new Set()
|
|
9555
|
+
) {
|
|
9556
|
+
if (maxDepth < 0) return { verdict: 'unknown' };
|
|
9557
|
+
const stateKey = `${startAbs}\x00${exposedName}`;
|
|
9558
|
+
if (visited.has(stateKey)) return { verdict: 'unknown' };
|
|
9559
|
+
visited.add(stateKey);
|
|
9560
|
+
const fileEntry = index.files.get(startAbs);
|
|
9561
|
+
if (!fileEntry) return { verdict: 'unknown' };
|
|
9562
|
+
const details = fileEntry.exportDetails || [];
|
|
9563
|
+
if (details.some(item => item.type === 'exports' ||
|
|
9564
|
+
item.type === 'module.exports')) return { verdict: 'unknown' };
|
|
9565
|
+
|
|
9566
|
+
const local = details.filter(item =>
|
|
9567
|
+
!item.source && (item.alias || item.name) === exposedName);
|
|
9568
|
+
if (local.length > 0) {
|
|
9569
|
+
if (local.length !== 1 || !local[0].isVariable ||
|
|
9570
|
+
!local[0].typeAnnotation) return { verdict: 'unknown' };
|
|
9571
|
+
const type = _structuralTypeHead(local[0].typeAnnotation, {
|
|
9572
|
+
index,
|
|
9573
|
+
language,
|
|
9574
|
+
originFile: startAbs,
|
|
9575
|
+
});
|
|
9576
|
+
if (!type || _STRUCTURAL_FLOW_REJECT.has(type) ||
|
|
9577
|
+
(/^[A-Z][A-Z0-9]?$/.test(type) &&
|
|
9578
|
+
!(index.symbols.get(type) || [])
|
|
9579
|
+
.some(definition => IDENTITY_TYPE_KINDS.has(definition.type)))) {
|
|
9580
|
+
return { verdict: 'unknown' };
|
|
9581
|
+
}
|
|
9582
|
+
const typeDefs = (index.symbols.get(type) || [])
|
|
9583
|
+
.filter(definition => IDENTITY_TYPE_KINDS.has(definition.type));
|
|
9584
|
+
if (typeDefs.length === 0) return { verdict: 'yes', type };
|
|
9585
|
+
const origin = _resolveFlowTypeOrigin(index, startAbs, type);
|
|
9586
|
+
if (!origin?.fromFile) return { verdict: 'unknown' };
|
|
9587
|
+
return { verdict: 'yes', type, fromFile: origin.fromFile };
|
|
9588
|
+
}
|
|
9589
|
+
|
|
9590
|
+
const resolveSource = (item, name) => {
|
|
9591
|
+
const rel = fileEntry.moduleResolved?.[item.source];
|
|
9592
|
+
if (!rel) return { verdict: 'unknown' };
|
|
9593
|
+
return _structuralExportedValueType(
|
|
9594
|
+
index, path.join(index.root, rel), name, language,
|
|
9595
|
+
maxDepth - 1, new Set(visited));
|
|
9596
|
+
};
|
|
9597
|
+
const merge = results => {
|
|
9598
|
+
if (results.some(result => result.verdict === 'unknown')) {
|
|
9599
|
+
return { verdict: 'unknown' };
|
|
9600
|
+
}
|
|
9601
|
+
const matches = results.filter(result => result.verdict === 'yes');
|
|
9602
|
+
if (matches.length === 0) return { verdict: 'no' };
|
|
9603
|
+
const identities = new Set(matches.map(result =>
|
|
9604
|
+
`${result.type}\x00${result.fromFile || ''}`));
|
|
9605
|
+
return identities.size === 1 ? matches[0] : { verdict: 'unknown' };
|
|
9606
|
+
};
|
|
9607
|
+
|
|
9608
|
+
const exact = details.filter(item => item.source &&
|
|
9609
|
+
item.type === 're-export' && (item.alias || item.name) === exposedName);
|
|
9610
|
+
if (exact.length > 0) {
|
|
9611
|
+
return merge(exact.map(item => resolveSource(item, item.name)));
|
|
9612
|
+
}
|
|
9613
|
+
const stars = details.filter(item => item.source &&
|
|
9614
|
+
item.type === 're-export-all' && !item.alias);
|
|
9615
|
+
if (stars.length === 0) return { verdict: 'no' };
|
|
9616
|
+
return merge(stars.map(item => resolveSource(item, exposedName)));
|
|
9617
|
+
}
|
|
9618
|
+
|
|
9619
|
+
/**
|
|
9620
|
+
* Type a one-hop field receiver rooted at an unshadowed namespace import:
|
|
9621
|
+
* `api.service.run()` where `service` is an explicitly typed exported value.
|
|
9622
|
+
*/
|
|
9623
|
+
function _structuralModuleValueFieldType(index, fileEntry, call, info = null) {
|
|
9624
|
+
if (!call?.receiverRoot || !call.receiverField || call.receiverLocalBinding) {
|
|
9625
|
+
return null;
|
|
9626
|
+
}
|
|
9627
|
+
const fields = call.receiverFields || [call.receiverField];
|
|
9628
|
+
if (fields.length !== 1) return null;
|
|
9629
|
+
const cache = index._opImportReachCache;
|
|
9630
|
+
const cacheKey = `module-value-type\x00${fileEntry?.path || ''}\x00` +
|
|
9631
|
+
`${call.receiverRoot}\x00${call.receiverField}`;
|
|
9632
|
+
if (cache?.has(cacheKey)) {
|
|
9633
|
+
const cached = cache.get(cacheKey);
|
|
9634
|
+
if (info && cached?.fromFile) info.fromFile = cached.fromFile;
|
|
9635
|
+
return cached?.type || null;
|
|
9636
|
+
}
|
|
9637
|
+
const finish = result => {
|
|
9638
|
+
if (cache) cache.set(cacheKey, result);
|
|
9639
|
+
if (info && result?.fromFile) info.fromFile = result.fromFile;
|
|
9640
|
+
return result?.type || null;
|
|
9641
|
+
};
|
|
9642
|
+
const bindings = (fileEntry?.importBindings || []).filter(binding =>
|
|
9643
|
+
binding.kind === 'namespace' &&
|
|
9644
|
+
(binding.alias || binding.name) === call.receiverRoot);
|
|
9645
|
+
if (bindings.length === 0) return finish(null);
|
|
9646
|
+
|
|
9647
|
+
const results = [];
|
|
9648
|
+
for (const binding of bindings) {
|
|
9649
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
9650
|
+
if (!rel) return finish(null);
|
|
9651
|
+
const result = _structuralExportedValueType(
|
|
9652
|
+
index, path.join(index.root, rel), call.receiverField,
|
|
9653
|
+
fileEntry.language);
|
|
9654
|
+
if (result.verdict !== 'yes') return finish(null);
|
|
9655
|
+
results.push(result);
|
|
9656
|
+
}
|
|
9657
|
+
const identities = new Set(results.map(result =>
|
|
9658
|
+
`${result.type}\x00${result.fromFile || ''}`));
|
|
9659
|
+
if (identities.size !== 1) return finish(null);
|
|
9660
|
+
return finish(results[0]);
|
|
9661
|
+
}
|
|
9662
|
+
|
|
9146
9663
|
/**
|
|
9147
9664
|
* Ownership chase for a CommonJS default-like require binding:
|
|
9148
9665
|
* `const local = require('./module')`. The local binding name says nothing
|
|
@@ -9373,6 +9890,21 @@ function _genericParamNames(genericsText) {
|
|
|
9373
9890
|
return names.size > 0 ? names : null;
|
|
9374
9891
|
}
|
|
9375
9892
|
|
|
9893
|
+
/**
|
|
9894
|
+
* A method declared on a generic owner parameter (`impl<I> Trait for I`) has
|
|
9895
|
+
* no concrete receiver identity. The owner may be instantiated by every type
|
|
9896
|
+
* satisfying the impl bounds, so it can neither confirm nor exclude a typed
|
|
9897
|
+
* receiver. Parser-provided ownerGenerics distinguishes this from an actual
|
|
9898
|
+
* project type whose short name happens to look generic.
|
|
9899
|
+
*/
|
|
9900
|
+
function _definitionHasGenericOwner(definition) {
|
|
9901
|
+
const owner = definition?.className ||
|
|
9902
|
+
(definition?.receiver || '').replace(/^[*&]\s*/, '');
|
|
9903
|
+
if (!owner) return false;
|
|
9904
|
+
const params = _genericParamNames(definition.ownerGenerics);
|
|
9905
|
+
return !!params?.has(owner);
|
|
9906
|
+
}
|
|
9907
|
+
|
|
9376
9908
|
/**
|
|
9377
9909
|
* Is typeName a declared GENERIC TYPE PARAMETER in scope at this call site —
|
|
9378
9910
|
* on the enclosing function itself (`fn f<TStore: Wipe>(t: &TStore)`) or on
|
|
@@ -10241,6 +10773,62 @@ function _structuralModuleBindings(fileEntry, call) {
|
|
|
10241
10773
|
return (fileEntry?.importBindings || []).filter(b => b.name === call?.receiver);
|
|
10242
10774
|
}
|
|
10243
10775
|
|
|
10776
|
+
/**
|
|
10777
|
+
* Resolve an ordinary module-local object composed from namespace spreads:
|
|
10778
|
+
* `const z = { ...schemas, ...checks, iso }`.
|
|
10779
|
+
*
|
|
10780
|
+
* The JS parser records this only for a private, unescaped, unmodified const
|
|
10781
|
+
* object. Walk layers from last to first because later object spreads and
|
|
10782
|
+
* explicit properties override earlier names. A layer may be skipped only
|
|
10783
|
+
* when its complete modeled export surface definitively lacks the requested
|
|
10784
|
+
* name; resolver gaps and dynamic surfaces remain unknown.
|
|
10785
|
+
*
|
|
10786
|
+
* When targetFiles is supplied, verdict answers whether the winning layer
|
|
10787
|
+
* owns that pinned definition. Without targetFiles it returns the winning
|
|
10788
|
+
* module binding for return-flow/callee lookup.
|
|
10789
|
+
*/
|
|
10790
|
+
function _structuralCompositeModuleOwnership(
|
|
10791
|
+
index, fileEntry, call, targetFiles = null
|
|
10792
|
+
) {
|
|
10793
|
+
const layers = call?.receiverModuleComposition;
|
|
10794
|
+
if (!Array.isArray(layers) || layers.length === 0 || !call.name) return null;
|
|
10795
|
+
const nameFiles = new Set((index.symbols.get(call.name) || [])
|
|
10796
|
+
.filter(definition => definition.file &&
|
|
10797
|
+
(!NON_CALLABLE_TYPES.has(definition.type) || definition.type === 'class'))
|
|
10798
|
+
.map(definition => definition.file));
|
|
10799
|
+
if (nameFiles.size === 0) return { verdict: 'unknown' };
|
|
10800
|
+
|
|
10801
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
10802
|
+
const layer = layers[i];
|
|
10803
|
+
if (layer.kind === 'property') {
|
|
10804
|
+
if (layer.name === call.name) return { verdict: 'unknown' };
|
|
10805
|
+
continue;
|
|
10806
|
+
}
|
|
10807
|
+
if (layer.kind !== 'spread' || !layer.receiver) {
|
|
10808
|
+
return { verdict: 'unknown' };
|
|
10809
|
+
}
|
|
10810
|
+
const bindings = (fileEntry?.importBindings || []).filter(binding =>
|
|
10811
|
+
(binding.alias || binding.name) === layer.receiver &&
|
|
10812
|
+
binding.kind === 'namespace');
|
|
10813
|
+
if (bindings.length !== 1) return { verdict: 'unknown' };
|
|
10814
|
+
const binding = bindings[0];
|
|
10815
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
10816
|
+
if (!rel) return { verdict: 'unknown' };
|
|
10817
|
+
const moduleFile = path.join(index.root, rel);
|
|
10818
|
+
const presence = _nameBindingReaches(
|
|
10819
|
+
index, moduleFile, call.name, nameFiles);
|
|
10820
|
+
if (presence === 'unknown') return { verdict: 'unknown' };
|
|
10821
|
+
if (presence === 'no') continue;
|
|
10822
|
+
if (!targetFiles) {
|
|
10823
|
+
return { verdict: 'yes', binding, rel, moduleFile };
|
|
10824
|
+
}
|
|
10825
|
+
const verdict = _nameBindingReaches(
|
|
10826
|
+
index, moduleFile, call.name, targetFiles);
|
|
10827
|
+
return { verdict, binding, rel, moduleFile };
|
|
10828
|
+
}
|
|
10829
|
+
return { verdict: 'no' };
|
|
10830
|
+
}
|
|
10831
|
+
|
|
10244
10832
|
function _pythonBuiltinContractAllowed(index, fileEntry, moduleName) {
|
|
10245
10833
|
const module = String(moduleName || '');
|
|
10246
10834
|
if (!module || module.startsWith('.')) return false;
|
|
@@ -10341,7 +10929,12 @@ function _structuralReturnedConstructorFlow(index, definition) {
|
|
|
10341
10929
|
}
|
|
10342
10930
|
|
|
10343
10931
|
function _calleeStructuralModuleRoute(index, fileEntry, call, language) {
|
|
10344
|
-
const
|
|
10932
|
+
const composite = call.receiverModuleComposition
|
|
10933
|
+
? _structuralCompositeModuleOwnership(index, fileEntry, call)
|
|
10934
|
+
: null;
|
|
10935
|
+
if (composite && composite.verdict !== 'yes') return { unknown: true };
|
|
10936
|
+
const bindings = composite
|
|
10937
|
+
? [composite.binding] : _structuralModuleBindings(fileEntry, call);
|
|
10345
10938
|
if (bindings.length === 0) return { unknown: true };
|
|
10346
10939
|
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, call.name, false);
|
|
10347
10940
|
}
|
|
@@ -10641,8 +11234,15 @@ function _calleeSelectReceiverMethod(index, call, symbols, typeName, language,
|
|
|
10641
11234
|
(symbol.receiver && symbol.receiver.replace(/^\*/, '') === owner)));
|
|
10642
11235
|
let selected = _calleeOverloadSelect(
|
|
10643
11236
|
index, call, onOwner(typeName), language);
|
|
10644
|
-
|
|
10645
|
-
|
|
11237
|
+
// Nominal lookup and compiler-typed TS/TSX class values inherit methods
|
|
11238
|
+
// from the nearest recorded base. Structural lookup stays restricted to
|
|
11239
|
+
// TypeScript here: plain JavaScript/Python receivers can be dynamically
|
|
11240
|
+
// reshaped, while a TS return annotation plus a concrete class heritage
|
|
11241
|
+
// edge is compiler-grade method ownership.
|
|
11242
|
+
const followsRecordedClassInheritance =
|
|
11243
|
+
langTraits(language)?.typeSystem === 'nominal' ||
|
|
11244
|
+
language === 'typescript' || language === 'tsx';
|
|
11245
|
+
if (!selected.match && !selected.ambiguous && followsRecordedClassInheritance) {
|
|
10646
11246
|
const visited = new Set([typeName]);
|
|
10647
11247
|
const queue = [...(index._getInheritanceParents?.(
|
|
10648
11248
|
typeName, contextFile) || [])];
|
|
@@ -10695,7 +11295,97 @@ function _csharpParamsNormalFormApplicable(index, call, definition) {
|
|
|
10695
11295
|
return !!actual && !!expected && actual === expected;
|
|
10696
11296
|
}
|
|
10697
11297
|
|
|
10698
|
-
function
|
|
11298
|
+
function _goContainerElementType(raw) {
|
|
11299
|
+
const text = String(raw || '').trim();
|
|
11300
|
+
let element = null;
|
|
11301
|
+
const bracketStart = text.startsWith('map[') ? 3 : text.startsWith('[') ? 0 : -1;
|
|
11302
|
+
if (bracketStart >= 0) {
|
|
11303
|
+
let depth = 0;
|
|
11304
|
+
for (let i = bracketStart; i < text.length; i++) {
|
|
11305
|
+
if (text[i] === '[') depth++;
|
|
11306
|
+
else if (text[i] === ']') {
|
|
11307
|
+
depth--;
|
|
11308
|
+
if (depth === 0) {
|
|
11309
|
+
element = text.slice(i + 1).trim();
|
|
11310
|
+
break;
|
|
11311
|
+
}
|
|
11312
|
+
}
|
|
11313
|
+
}
|
|
11314
|
+
}
|
|
11315
|
+
if (!element) return null;
|
|
11316
|
+
element = element.replace(/^\*+/, '').trim();
|
|
11317
|
+
const qualified = element.match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
|
|
11318
|
+
if (qualified) {
|
|
11319
|
+
return { qualifier: qualified[1], type: qualified[2] };
|
|
11320
|
+
}
|
|
11321
|
+
return /^[A-Za-z_]\w*$/.test(element)
|
|
11322
|
+
? { qualifier: null, type: element } : null;
|
|
11323
|
+
}
|
|
11324
|
+
|
|
11325
|
+
function _goIndexedReceiverType(index, filePath, call) {
|
|
11326
|
+
const rootType = call.receiverIndexRootType;
|
|
11327
|
+
const fieldName = call.receiverIndexField;
|
|
11328
|
+
if (!rootType || !fieldName) return null;
|
|
11329
|
+
|
|
11330
|
+
const rootOrigin = _resolveFlowTypeOrigin(
|
|
11331
|
+
index, filePath, rootType, call.receiverIndexRootTypeQualifier);
|
|
11332
|
+
if (call.receiverIndexRootTypeQualifier && !rootOrigin?.fromFile) {
|
|
11333
|
+
return { externalVia: `${call.receiverIndexRootTypeQualifier}.${rootType}` };
|
|
11334
|
+
}
|
|
11335
|
+
|
|
11336
|
+
let fields = (index.symbols.get(fieldName) || []).filter(definition =>
|
|
11337
|
+
definition.className === rootType && definition.fieldType &&
|
|
11338
|
+
(definition.type === 'field' || definition.memberType === 'field'));
|
|
11339
|
+
if (rootOrigin?.fromFile) {
|
|
11340
|
+
const owned = fields.filter(field => field.file === rootOrigin.fromFile);
|
|
11341
|
+
if (owned.length > 0) fields = owned;
|
|
11342
|
+
}
|
|
11343
|
+
if (fields.length === 0) return null;
|
|
11344
|
+
|
|
11345
|
+
const candidates = new Map();
|
|
11346
|
+
for (const field of fields) {
|
|
11347
|
+
const element = _goContainerElementType(field.fieldType);
|
|
11348
|
+
if (!element) return null;
|
|
11349
|
+
if (element.qualifier) {
|
|
11350
|
+
const origin = field.file && _resolveFlowTypeOrigin(
|
|
11351
|
+
index, field.file, element.type, element.qualifier);
|
|
11352
|
+
if (origin?.fromFile) {
|
|
11353
|
+
candidates.set(`${element.type}\0${origin.fromFile}`, {
|
|
11354
|
+
type: element.type,
|
|
11355
|
+
fromFile: origin.fromFile,
|
|
11356
|
+
});
|
|
11357
|
+
} else if (field.file &&
|
|
11358
|
+
_goQualifierNamesImport(index, field.file, element.qualifier)) {
|
|
11359
|
+
candidates.set(`external\0${element.qualifier}.${element.type}`, {
|
|
11360
|
+
externalVia: `${element.qualifier}.${element.type}`,
|
|
11361
|
+
});
|
|
11362
|
+
} else {
|
|
11363
|
+
return null;
|
|
11364
|
+
}
|
|
11365
|
+
continue;
|
|
11366
|
+
}
|
|
11367
|
+
|
|
11368
|
+
const typeDefs = (index.symbols.get(element.type) || [])
|
|
11369
|
+
.filter(definition => IDENTITY_TYPE_KINDS.has(definition.type));
|
|
11370
|
+
if (typeDefs.length === 0) {
|
|
11371
|
+
if (!BUILTIN_RECEIVER_TYPES.has(element.type)) return null;
|
|
11372
|
+
candidates.set(`builtin\0${element.type}`, { type: element.type });
|
|
11373
|
+
continue;
|
|
11374
|
+
}
|
|
11375
|
+
const origin = field.file && _resolveFlowTypeOrigin(
|
|
11376
|
+
index, field.file, element.type);
|
|
11377
|
+
if (!origin?.fromFile) return null;
|
|
11378
|
+
candidates.set(`${element.type}\0${origin.fromFile}`, {
|
|
11379
|
+
type: element.type,
|
|
11380
|
+
fromFile: origin.fromFile,
|
|
11381
|
+
});
|
|
11382
|
+
}
|
|
11383
|
+
return candidates.size === 1 ? [...candidates.values()][0] : null;
|
|
11384
|
+
}
|
|
11385
|
+
|
|
11386
|
+
function _declaredFieldType(
|
|
11387
|
+
index, rootType, fieldName, language, info = null, rootNamespace = undefined
|
|
11388
|
+
) {
|
|
10699
11389
|
const defs = index.symbols.get(fieldName) || [];
|
|
10700
11390
|
if (defs.length === 0 && language !== 'python') return null;
|
|
10701
11391
|
// 'private field' (JS #-fields, fix #219): equally compiler-true, and
|
|
@@ -10708,7 +11398,7 @@ function _declaredFieldType(index, rootType, fieldName, language, info, rootName
|
|
|
10708
11398
|
d.type === 'property' || d.memberType === 'property';
|
|
10709
11399
|
const fields = defs.filter(d =>
|
|
10710
11400
|
((d.type === 'field' || d.memberType === 'field' || d.memberType === 'private field') && d.fieldType) ||
|
|
10711
|
-
(isAccessor(d) && d.returnType));
|
|
11401
|
+
(isAccessor(d) && (d.returnType || d.fieldType)));
|
|
10712
11402
|
let onType = fields.filter(d => d.className === rootType &&
|
|
10713
11403
|
(language !== 'csharp' || !rootNamespace ||
|
|
10714
11404
|
(d.namespace || null) === rootNamespace));
|
|
@@ -10767,7 +11457,7 @@ function _declaredFieldType(index, rootType, fieldName, language, info, rootName
|
|
|
10767
11457
|
if (onType.length === 0) return null;
|
|
10768
11458
|
const normalized = new Set();
|
|
10769
11459
|
for (const f of onType) {
|
|
10770
|
-
const rawText = isAccessor(f) ? f.returnType : f.fieldType;
|
|
11460
|
+
const rawText = isAccessor(f) ? (f.returnType || f.fieldType) : f.fieldType;
|
|
10771
11461
|
// Qualified declared types resolve through the FIELD-DECLARING file's
|
|
10772
11462
|
// imports or not at all (fix #268, chi-measured — the #206 identity
|
|
10773
11463
|
// discipline): `inner http.Handler` is net/http's Handler, never a
|
|
@@ -10799,7 +11489,10 @@ function _declaredFieldType(index, rootType, fieldName, language, info, rootName
|
|
|
10799
11489
|
return null;
|
|
10800
11490
|
}
|
|
10801
11491
|
}
|
|
10802
|
-
const localType = _normalizeFieldTypeName(rawText, language
|
|
11492
|
+
const localType = _normalizeFieldTypeName(rawText, language, {
|
|
11493
|
+
index,
|
|
11494
|
+
originFile: f.file,
|
|
11495
|
+
});
|
|
10803
11496
|
const importedIdentity = language === 'rust' && f.file && localType
|
|
10804
11497
|
? _rustImportedTypeIdentity(index, f.file, localType) : null;
|
|
10805
11498
|
const t = importedIdentity?.type || localType;
|
|
@@ -10824,11 +11517,15 @@ function _declaredFieldType(index, rootType, fieldName, language, info, rootName
|
|
|
10824
11517
|
complete = false;
|
|
10825
11518
|
break;
|
|
10826
11519
|
}
|
|
10827
|
-
const rawText = isAccessor(field)
|
|
11520
|
+
const rawText = isAccessor(field)
|
|
11521
|
+
? (field.returnType || field.fieldType) : field.fieldType;
|
|
10828
11522
|
const qualifier = language === 'java'
|
|
10829
11523
|
? _javaNestedTypeQualifier(rawText) : undefined;
|
|
10830
11524
|
if (qualifier) namespaces.add(qualifier);
|
|
10831
|
-
const localType = _normalizeFieldTypeName(rawText, language
|
|
11525
|
+
const localType = _normalizeFieldTypeName(rawText, language, {
|
|
11526
|
+
index,
|
|
11527
|
+
originFile: field.file,
|
|
11528
|
+
});
|
|
10832
11529
|
const importedIdentity = language === 'rust' && localType
|
|
10833
11530
|
? _rustImportedTypeIdentity(index, field.file, localType) : null;
|
|
10834
11531
|
const origin = importedIdentity?.type === typeName
|
|
@@ -10983,10 +11680,14 @@ function _nonCallableFieldMember(index, typeName, name, language) {
|
|
|
10983
11680
|
(d.receiver && d.receiver.replace(/^\*/, '') === typeName));
|
|
10984
11681
|
if (onType.length === 0) return false;
|
|
10985
11682
|
for (const d of onType) {
|
|
10986
|
-
|
|
10987
|
-
|
|
11683
|
+
const valueMember = d.type === 'field' || d.memberType === 'field' ||
|
|
11684
|
+
d.memberType === 'private field' || d.type === 'property' ||
|
|
11685
|
+
d.memberType === 'property';
|
|
11686
|
+
if (!valueMember) return false;
|
|
11687
|
+
const declaredType = d.fieldType || d.returnType;
|
|
11688
|
+
if (!declaredType) return false;
|
|
10988
11689
|
if (_callableFieldDef(index, d)) return false;
|
|
10989
|
-
const raw = String(
|
|
11690
|
+
const raw = String(declaredType).trim();
|
|
10990
11691
|
if (/^func\b/.test(raw)) return false;
|
|
10991
11692
|
if (/\bfn\s*\(|\b(?:Fn|FnMut|FnOnce)\s*[(<]/.test(raw)) return false;
|
|
10992
11693
|
if (langTraits(language)?.typeSystem === 'structural') {
|
|
@@ -12185,8 +12886,17 @@ function _javaArgKindMatches(index, kind, paramType, language) {
|
|
|
12185
12886
|
return allowed.includes(bare);
|
|
12186
12887
|
}
|
|
12187
12888
|
|
|
12188
|
-
function _cppTypeCategory(type) {
|
|
12889
|
+
function _cppTypeCategory(index, type) {
|
|
12189
12890
|
if (!type) return { kind: 'unknown', head: null };
|
|
12891
|
+
const cacheKey = String(type);
|
|
12892
|
+
const cached = index._opCppTypeCategoryCache?.get(cacheKey);
|
|
12893
|
+
if (cached) return cached;
|
|
12894
|
+
const result = _computeCppTypeCategory(type);
|
|
12895
|
+
index._opCppTypeCategoryCache?.set(cacheKey, result);
|
|
12896
|
+
return result;
|
|
12897
|
+
}
|
|
12898
|
+
|
|
12899
|
+
function _computeCppTypeCategory(type) {
|
|
12190
12900
|
const original = String(type).trim();
|
|
12191
12901
|
const compact = original.replace(/\s+/g, '');
|
|
12192
12902
|
const unqualified = original
|
|
@@ -12266,9 +12976,9 @@ function _cppTypeCategory(type) {
|
|
|
12266
12976
|
* converting constructors and unknown template constraints keep the
|
|
12267
12977
|
* candidate alive.
|
|
12268
12978
|
*/
|
|
12269
|
-
function _cppArgKindMatches(kind, paramType) {
|
|
12979
|
+
function _cppArgKindMatches(index, kind, paramType) {
|
|
12270
12980
|
if (!kind || kind === 'expr' || !paramType) return true;
|
|
12271
|
-
const expected = _cppTypeCategory(paramType);
|
|
12981
|
+
const expected = _cppTypeCategory(index, paramType);
|
|
12272
12982
|
if (expected.kind === 'unknown' || expected.kind === 'generic') return true;
|
|
12273
12983
|
|
|
12274
12984
|
if (kind.startsWith('string:')) {
|
|
@@ -12315,7 +13025,7 @@ function _cppArgKindMatches(kind, paramType) {
|
|
|
12315
13025
|
if (kind.startsWith('type:') || kind.startsWith('call:') ||
|
|
12316
13026
|
kind.startsWith('bcall:')) {
|
|
12317
13027
|
const actualType = kind.slice(kind.indexOf(':') + 1);
|
|
12318
|
-
const actual = _cppTypeCategory(actualType);
|
|
13028
|
+
const actual = _cppTypeCategory(index, actualType);
|
|
12319
13029
|
if (actual.head && expected.head && actual.head === expected.head) return true;
|
|
12320
13030
|
const closed = new Set([
|
|
12321
13031
|
'format-string', 'string', 'locale', 'style',
|
|
@@ -12357,7 +13067,7 @@ function _overloadApplicable(index, call, def) {
|
|
|
12357
13067
|
const p = ps[i];
|
|
12358
13068
|
if (!p || p.rest) break;
|
|
12359
13069
|
const matches = language === 'cpp'
|
|
12360
|
-
? _cppArgKindMatches(kinds[i], p.type)
|
|
13070
|
+
? _cppArgKindMatches(index, kinds[i], p.type)
|
|
12361
13071
|
: _javaArgKindMatches(index, kinds[i], p.type, language);
|
|
12362
13072
|
if (!matches) return false;
|
|
12363
13073
|
}
|
|
@@ -12651,6 +13361,10 @@ function _cppTargetVisibility(index, callerFile, targetDefs) {
|
|
|
12651
13361
|
|
|
12652
13362
|
function _cppPathReceiverNamesType(index, receiver) {
|
|
12653
13363
|
if (!receiver) return false;
|
|
13364
|
+
const cacheKey = String(receiver);
|
|
13365
|
+
if (index._opCppPathReceiverTypeCache?.has(cacheKey)) {
|
|
13366
|
+
return index._opCppPathReceiverTypeCache.get(cacheKey);
|
|
13367
|
+
}
|
|
12654
13368
|
// Remove template arguments before selecting the terminal path segment;
|
|
12655
13369
|
// nested qualifiers inside `<...>` must not be mistaken for the owner.
|
|
12656
13370
|
let plain = '';
|
|
@@ -12667,9 +13381,11 @@ function _cppPathReceiverNamesType(index, receiver) {
|
|
|
12667
13381
|
if (depth === 0) plain += character;
|
|
12668
13382
|
}
|
|
12669
13383
|
const name = plain.split('::').filter(Boolean).pop();
|
|
12670
|
-
|
|
13384
|
+
const result = !!(name && (index.symbols.get(name) || []).some(definition =>
|
|
12671
13385
|
IDENTITY_TYPE_KINDS.has(definition.type) ||
|
|
12672
13386
|
(definition.type === 'type' && definition.aliasOf)));
|
|
13387
|
+
index._opCppPathReceiverTypeCache?.set(cacheKey, result);
|
|
13388
|
+
return result;
|
|
12673
13389
|
}
|
|
12674
13390
|
|
|
12675
13391
|
function _cppQualifiedPathOwnsTarget(index, callerFile, call, targetDefs) {
|
|
@@ -13125,13 +13841,38 @@ function _cppExactOverloadWinner(call, applicable) {
|
|
|
13125
13841
|
*/
|
|
13126
13842
|
function _buildTargetTypeSet(index, targetDefs, definitions) {
|
|
13127
13843
|
const targetTypes = new Set();
|
|
13844
|
+
const targetTypeOrigins = [];
|
|
13845
|
+
// Callable-identity closure joins a trait declaration with its impl slot.
|
|
13846
|
+
// If any member is a blanket impl over a generic owner, the whole slot is
|
|
13847
|
+
// universally quantified: a concrete receiver can satisfy it without
|
|
13848
|
+
// having the trait name as its nominal class. No member of that closed
|
|
13849
|
+
// target group has exclusion-grade concrete receiver identity.
|
|
13850
|
+
if (targetDefs.some(_definitionHasGenericOwner)) return targetTypes;
|
|
13128
13851
|
for (const td of targetDefs) {
|
|
13852
|
+
// Blanket/generic impl owner (`impl<I> Trait for I`) is a quantified
|
|
13853
|
+
// parameter, not a concrete type called I. Keeping it in targetTypes
|
|
13854
|
+
// made every real receiver look provably unrelated and excluded true
|
|
13855
|
+
// Rayon par_iter/par_iter_mut calls. Empty target identity deliberately
|
|
13856
|
+
// falls through to the visible dispatch tier.
|
|
13129
13857
|
if (td.explicitInterface) {
|
|
13130
13858
|
const interfaceType = _csharpTypeIdentity(td.explicitInterface);
|
|
13131
|
-
if (interfaceType)
|
|
13859
|
+
if (interfaceType) {
|
|
13860
|
+
targetTypes.add(interfaceType);
|
|
13861
|
+
targetTypeOrigins.push({ name: interfaceType, file: td.file });
|
|
13862
|
+
}
|
|
13132
13863
|
} else {
|
|
13133
|
-
|
|
13134
|
-
|
|
13864
|
+
const owners = [
|
|
13865
|
+
td.className,
|
|
13866
|
+
td.receiver && td.receiver.replace(/^\*/, ''),
|
|
13867
|
+
].filter(Boolean);
|
|
13868
|
+
for (const owner of owners) {
|
|
13869
|
+
targetTypes.add(owner);
|
|
13870
|
+
const origin = _resolveFlowTypeOrigin(index, td.file, owner);
|
|
13871
|
+
targetTypeOrigins.push({
|
|
13872
|
+
name: owner,
|
|
13873
|
+
file: origin?.fromFile || td.file,
|
|
13874
|
+
});
|
|
13875
|
+
}
|
|
13135
13876
|
}
|
|
13136
13877
|
}
|
|
13137
13878
|
if (targetTypes.size > 0) {
|
|
@@ -13148,26 +13889,42 @@ function _buildTargetTypeSet(index, targetDefs, definitions) {
|
|
|
13148
13889
|
const overloadedSlots = !!langTraits(language)?.hasArityOverloads;
|
|
13149
13890
|
const targetSignatures = new Set(targetDefs.map(signature)
|
|
13150
13891
|
.filter(value => value !== null));
|
|
13151
|
-
const queue =
|
|
13892
|
+
const queue = targetTypeOrigins.length > 0
|
|
13893
|
+
? targetTypeOrigins : [...targetTypes].map(name => ({ name }));
|
|
13152
13894
|
while (queue.length > 0) {
|
|
13153
|
-
const
|
|
13895
|
+
const parent = queue.pop();
|
|
13896
|
+
const children = index.extendedByGraph?.get(parent.name);
|
|
13154
13897
|
if (!children) continue;
|
|
13155
13898
|
for (const child of children) {
|
|
13156
13899
|
const cName = typeof child === 'string' ? child : child.name;
|
|
13157
13900
|
if (!cName || targetTypes.has(cName)) continue;
|
|
13901
|
+
const childFile = typeof child === 'string' ? null : child.file;
|
|
13902
|
+
if (parent.file && childFile) {
|
|
13903
|
+
// extendedByGraph is keyed by the parent's SHORT name.
|
|
13904
|
+
// Parallel package versions can therefore share a bucket
|
|
13905
|
+
// (zod v3/v4 both define ZodType). Admit a child only when
|
|
13906
|
+
// its written parent resolves back to this exact target
|
|
13907
|
+
// type origin. An unresolved parent stays conservative;
|
|
13908
|
+
// a positively foreign origin is never confirmation-grade.
|
|
13909
|
+
const parentOrigin = _resolveFlowTypeOrigin(
|
|
13910
|
+
index, childFile, parent.name);
|
|
13911
|
+
if (parentOrigin?.fromFile &&
|
|
13912
|
+
parentOrigin.fromFile !== parent.file) continue;
|
|
13913
|
+
}
|
|
13158
13914
|
// In overload-capable languages, another same-named overload
|
|
13159
13915
|
// on the child does not override the pinned virtual slot.
|
|
13160
13916
|
// JsonTextWriter.WriteValue(Guid) must not hide inherited
|
|
13161
13917
|
// JsonWriter.WriteValue(Guid?). Only an agreeing parameter
|
|
13162
13918
|
// signature blocks the subtype closure.
|
|
13163
13919
|
const childDefinitions = definitions.filter(definition =>
|
|
13164
|
-
definition.className === cName
|
|
13920
|
+
definition.className === cName &&
|
|
13921
|
+
(!childFile || definition.file === childFile));
|
|
13165
13922
|
const overrides = childDefinitions.some(definition =>
|
|
13166
13923
|
!overloadedSlots ||
|
|
13167
13924
|
targetSignatures.has(signature(definition)));
|
|
13168
13925
|
if (overrides) continue;
|
|
13169
13926
|
targetTypes.add(cName);
|
|
13170
|
-
queue.push(cName);
|
|
13927
|
+
queue.push({ name: cName, file: childFile || parent.file });
|
|
13171
13928
|
}
|
|
13172
13929
|
}
|
|
13173
13930
|
}
|
|
@@ -13175,12 +13932,16 @@ function _buildTargetTypeSet(index, targetDefs, definitions) {
|
|
|
13175
13932
|
// callable through the wrapper value. Close only over wrappers whose
|
|
13176
13933
|
// indexed type definitions all agree on one Deref target.
|
|
13177
13934
|
if (targetTypes.size > 0) {
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13935
|
+
let derefPairs = index._opDerefPairs;
|
|
13936
|
+
if (!Array.isArray(derefPairs)) {
|
|
13937
|
+
derefPairs = [];
|
|
13938
|
+
for (const [wrapper, defs] of index.symbols) {
|
|
13939
|
+
const typeDefs = defs.filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
13940
|
+
if (typeDefs.length === 0 || !typeDefs.every(d => d.derefTarget)) continue;
|
|
13941
|
+
const targets = new Set(typeDefs.map(d => d.derefTarget));
|
|
13942
|
+
if (targets.size === 1) derefPairs.push([wrapper, [...targets][0]]);
|
|
13943
|
+
}
|
|
13944
|
+
if (index._opDerefPairs !== null) index._opDerefPairs = derefPairs;
|
|
13184
13945
|
}
|
|
13185
13946
|
let changed = derefPairs.length > 0;
|
|
13186
13947
|
while (changed) {
|
|
@@ -13204,19 +13965,23 @@ function _buildTargetTypeSet(index, targetDefs, definitions) {
|
|
|
13204
13965
|
// package must not confirm foreign receivers (#206 discipline). The
|
|
13205
13966
|
// parser records aliasOf for Rust/Go; names without it never close.
|
|
13206
13967
|
if (targetTypes.size > 0) {
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
if (
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13968
|
+
let aliasPairs = index._opAliasPairs;
|
|
13969
|
+
if (!Array.isArray(aliasPairs)) {
|
|
13970
|
+
aliasPairs = [];
|
|
13971
|
+
for (const [aliasName, defs] of index.symbols) {
|
|
13972
|
+
let base = null;
|
|
13973
|
+
let pure = true;
|
|
13974
|
+
for (const d of defs) {
|
|
13975
|
+
if (d.type !== 'type' && !IDENTITY_TYPE_KINDS.has(d.type)) continue;
|
|
13976
|
+
if (d.type === 'type' && d.aliasOf) {
|
|
13977
|
+
const normalized = _normalizedAliasBase(index, d);
|
|
13978
|
+
if (base === null) base = normalized;
|
|
13979
|
+
else if (base !== normalized) { pure = false; break; }
|
|
13980
|
+
} else { pure = false; break; }
|
|
13981
|
+
}
|
|
13982
|
+
if (pure && base) aliasPairs.push([aliasName, base]);
|
|
13983
|
+
}
|
|
13984
|
+
if (index._opAliasPairs !== null) index._opAliasPairs = aliasPairs;
|
|
13220
13985
|
}
|
|
13221
13986
|
let changed = aliasPairs.length > 0;
|
|
13222
13987
|
while (changed) {
|
|
@@ -13455,7 +14220,8 @@ function _declaredFieldInterfaceType(index, rootType, fieldName, language, rootN
|
|
|
13455
14220
|
const defs = index.symbols.get(fieldName);
|
|
13456
14221
|
if (!defs) return null;
|
|
13457
14222
|
const fields = defs.filter(d =>
|
|
13458
|
-
(d.type === 'field' || d.memberType === 'field'
|
|
14223
|
+
(d.type === 'field' || d.memberType === 'field' ||
|
|
14224
|
+
d.type === 'property' || d.memberType === 'property') &&
|
|
13459
14225
|
d.className === rootType && d.fieldType &&
|
|
13460
14226
|
(language !== 'csharp' || !rootNamespace ||
|
|
13461
14227
|
(d.namespace || null) === rootNamespace));
|
|
@@ -13586,7 +14352,7 @@ function _javaNestedTypeQualifier(raw) {
|
|
|
13586
14352
|
* go: `*ignore.Ig` → Ig; slices/maps/chans/funcs → null
|
|
13587
14353
|
* java: `java.util.List<Foo>` → List; arrays → null
|
|
13588
14354
|
*/
|
|
13589
|
-
function _normalizeFieldTypeName(raw, language) {
|
|
14355
|
+
function _normalizeFieldTypeName(raw, language, options = {}) {
|
|
13590
14356
|
let t = String(raw).trim();
|
|
13591
14357
|
if (language === 'rust') {
|
|
13592
14358
|
let prev;
|
|
@@ -13625,7 +14391,7 @@ function _normalizeFieldTypeName(raw, language) {
|
|
|
13625
14391
|
if (langTraits(language)?.typeSystem === 'structural') {
|
|
13626
14392
|
// JS/TS/Python (fix #219): compiler-true annotation heads, value-
|
|
13627
14393
|
// position semantics — a field declared Promise<X> HOLDS a Promise.
|
|
13628
|
-
return _structuralTypeHead(t, { language });
|
|
14394
|
+
return _structuralTypeHead(t, { language, ...options });
|
|
13629
14395
|
}
|
|
13630
14396
|
return null;
|
|
13631
14397
|
}
|
|
@@ -13868,6 +14634,50 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
|
|
|
13868
14634
|
return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
|
|
13869
14635
|
}
|
|
13870
14636
|
|
|
14637
|
+
/**
|
|
14638
|
+
* Type a Python subscript expression used as a method receiver from the
|
|
14639
|
+
* indexed container's declared `__getitem__` return contract.
|
|
14640
|
+
*
|
|
14641
|
+
* The root must resolve to an exact project type definition. This prevents a
|
|
14642
|
+
* globally unique project `__getitem__` (or a same-named local class) from
|
|
14643
|
+
* lending identity to an external or ambiguous container. The returned value
|
|
14644
|
+
* then follows the same origin-pinned structural return rails as an ordinary
|
|
14645
|
+
* method call.
|
|
14646
|
+
*/
|
|
14647
|
+
function _pythonIndexedReceiverType(
|
|
14648
|
+
index, filePath, call, getFlowMap, cache = null
|
|
14649
|
+
) {
|
|
14650
|
+
if (!call?.receiverSubscriptRoot) return null;
|
|
14651
|
+
let rootType = call.receiverSubscriptRootType;
|
|
14652
|
+
let rootFromFile;
|
|
14653
|
+
if (!rootType && typeof getFlowMap === 'function') {
|
|
14654
|
+
const flow = _lookupReturnTypeFlow(getFlowMap(), {
|
|
14655
|
+
...call,
|
|
14656
|
+
receiver: call.receiverSubscriptRoot,
|
|
14657
|
+
});
|
|
14658
|
+
if (flow?.type) {
|
|
14659
|
+
rootType = flow.type;
|
|
14660
|
+
rootFromFile = flow.fromFile;
|
|
14661
|
+
}
|
|
14662
|
+
}
|
|
14663
|
+
if (!rootType) return null;
|
|
14664
|
+
|
|
14665
|
+
const origin = _resolveFlowTypeOrigin(
|
|
14666
|
+
index, rootFromFile || filePath, rootType,
|
|
14667
|
+
call.receiverSubscriptRootTypeQualifier);
|
|
14668
|
+
if (!origin?.fromFile) return null;
|
|
14669
|
+
|
|
14670
|
+
const key = `${origin.fromFile}\0${rootType}`;
|
|
14671
|
+
if (cache?.has(key)) return cache.get(key);
|
|
14672
|
+
const result = _methodReturnOnType(
|
|
14673
|
+
index, rootType, origin.fromFile, '__getitem__', 'python', {
|
|
14674
|
+
filePath,
|
|
14675
|
+
selfType: rootType,
|
|
14676
|
+
});
|
|
14677
|
+
if (cache) cache.set(key, result || null);
|
|
14678
|
+
return result;
|
|
14679
|
+
}
|
|
14680
|
+
|
|
13871
14681
|
function _chainedReceiverType(index, call, language) {
|
|
13872
14682
|
const defs = (index.symbols.get(call.receiverCall) || [])
|
|
13873
14683
|
.filter(d => !NON_CALLABLE_TYPES.has(d.type));
|
|
@@ -13926,6 +14736,131 @@ function _chainedReceiverType(index, call, language) {
|
|
|
13926
14736
|
|
|
13927
14737
|
const _FOLD_TYPE_KINDS = new Set(['class', 'struct', 'enum', 'trait', 'interface', 'record', 'type', 'namespace']);
|
|
13928
14738
|
|
|
14739
|
+
function _structuralTypeExpression(text) {
|
|
14740
|
+
if (!text || typeof text !== 'string') return null;
|
|
14741
|
+
const source = text.trim();
|
|
14742
|
+
const match = source.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/);
|
|
14743
|
+
if (!match) return null;
|
|
14744
|
+
const qualifiedHead = match[1];
|
|
14745
|
+
const rest = source.slice(qualifiedHead.length).trim();
|
|
14746
|
+
if (!rest) {
|
|
14747
|
+
return { head: qualifiedHead.split('.').pop(), qualifiedHead, args: [], text: source };
|
|
14748
|
+
}
|
|
14749
|
+
if (!rest.startsWith('<') || !rest.endsWith('>')) return null;
|
|
14750
|
+
let depth = 0;
|
|
14751
|
+
for (let i = 0; i < rest.length; i++) {
|
|
14752
|
+
if (rest[i] === '<') depth++;
|
|
14753
|
+
else if (rest[i] === '>') depth--;
|
|
14754
|
+
if (depth === 0 && i !== rest.length - 1) return null;
|
|
14755
|
+
if (depth < 0) return null;
|
|
14756
|
+
}
|
|
14757
|
+
if (depth !== 0) return null;
|
|
14758
|
+
return {
|
|
14759
|
+
head: qualifiedHead.split('.').pop(),
|
|
14760
|
+
qualifiedHead,
|
|
14761
|
+
args: _splitTopLevelGenericArgs(rest.slice(1, -1)).map(arg => arg.trim()),
|
|
14762
|
+
text: source,
|
|
14763
|
+
};
|
|
14764
|
+
}
|
|
14765
|
+
|
|
14766
|
+
function _structuralGenericParameters(generics) {
|
|
14767
|
+
if (!generics || typeof generics !== 'string') return [];
|
|
14768
|
+
const source = generics.trim();
|
|
14769
|
+
if (!source.startsWith('<') || !source.endsWith('>')) return [];
|
|
14770
|
+
return _splitTopLevelGenericArgs(source.slice(1, -1)).map(part => {
|
|
14771
|
+
const match = part.trim().match(/^([A-Za-z_$][\w$]*)/);
|
|
14772
|
+
return match ? match[1] : null;
|
|
14773
|
+
}).filter(Boolean);
|
|
14774
|
+
}
|
|
14775
|
+
|
|
14776
|
+
function _substituteStructuralGenerics(text, bindings) {
|
|
14777
|
+
let result = String(text || '').trim();
|
|
14778
|
+
for (const [name, value] of [...bindings].sort((a, b) => b[0].length - a[0].length)) {
|
|
14779
|
+
if (!value || !_structuralTypeExpression(value)) continue;
|
|
14780
|
+
result = result.replace(new RegExp(`\\b${name}\\b`, 'g'), value);
|
|
14781
|
+
}
|
|
14782
|
+
return result;
|
|
14783
|
+
}
|
|
14784
|
+
|
|
14785
|
+
function _pinnedStructuralTypeDefinition(index, typeName, fromFile) {
|
|
14786
|
+
let defs = (index.symbols.get(typeName) || []).filter(definition =>
|
|
14787
|
+
IDENTITY_TYPE_KINDS.has(definition.type) && definition.file);
|
|
14788
|
+
if (fromFile) {
|
|
14789
|
+
const origin = _resolveFlowTypeOrigin(index, fromFile, typeName);
|
|
14790
|
+
if (!origin?.fromFile) return null;
|
|
14791
|
+
defs = defs.filter(definition => definition.file === origin.fromFile);
|
|
14792
|
+
}
|
|
14793
|
+
return defs.length === 1 ? defs[0] : null;
|
|
14794
|
+
}
|
|
14795
|
+
|
|
14796
|
+
function _singleStructuralParent(text) {
|
|
14797
|
+
if (!text) return null;
|
|
14798
|
+
let angle = 0, square = 0, paren = 0;
|
|
14799
|
+
for (const ch of String(text)) {
|
|
14800
|
+
if (ch === '<') angle++;
|
|
14801
|
+
else if (ch === '>') angle--;
|
|
14802
|
+
else if (ch === '[') square++;
|
|
14803
|
+
else if (ch === ']') square--;
|
|
14804
|
+
else if (ch === '(') paren++;
|
|
14805
|
+
else if (ch === ')') paren--;
|
|
14806
|
+
else if (ch === ',' && angle === 0 && square === 0 && paren === 0) return null;
|
|
14807
|
+
if (angle < 0 || square < 0 || paren < 0) return null;
|
|
14808
|
+
}
|
|
14809
|
+
return angle === 0 && square === 0 && paren === 0 ? String(text).trim() : null;
|
|
14810
|
+
}
|
|
14811
|
+
|
|
14812
|
+
function _structuralFieldTypeExpression(
|
|
14813
|
+
index, typeText, fromFile, fieldName, depth = 0
|
|
14814
|
+
) {
|
|
14815
|
+
if (depth > 12) return null;
|
|
14816
|
+
const expression = _structuralTypeExpression(typeText);
|
|
14817
|
+
if (!expression) return null;
|
|
14818
|
+
const definition = _pinnedStructuralTypeDefinition(
|
|
14819
|
+
index, expression.head, fromFile);
|
|
14820
|
+
if (!definition) return null;
|
|
14821
|
+
const params = _structuralGenericParameters(definition.generics);
|
|
14822
|
+
const bindings = new Map();
|
|
14823
|
+
for (let i = 0; i < params.length && i < expression.args.length; i++) {
|
|
14824
|
+
if (_structuralTypeExpression(expression.args[i])) {
|
|
14825
|
+
bindings.set(params[i], expression.args[i]);
|
|
14826
|
+
}
|
|
14827
|
+
}
|
|
14828
|
+
const fields = (index.symbols.get(fieldName) || []).filter(field =>
|
|
14829
|
+
field.className === expression.head && field.file === definition.file &&
|
|
14830
|
+
(field.type === 'field' || field.memberType === 'field' ||
|
|
14831
|
+
field.memberType === 'private field') && field.fieldType);
|
|
14832
|
+
if (fields.length > 0) {
|
|
14833
|
+
const resolved = new Set(fields.map(field =>
|
|
14834
|
+
_substituteStructuralGenerics(field.fieldType, bindings)));
|
|
14835
|
+
if (resolved.size !== 1) return null;
|
|
14836
|
+
const text = [...resolved][0];
|
|
14837
|
+
const parsed = _structuralTypeExpression(text);
|
|
14838
|
+
if (!parsed) return null;
|
|
14839
|
+
const origin = _resolveFlowTypeOrigin(index, definition.file, parsed.head);
|
|
14840
|
+
return { text, fromFile: origin?.fromFile || definition.file };
|
|
14841
|
+
}
|
|
14842
|
+
const parent = _singleStructuralParent(definition.extends);
|
|
14843
|
+
if (!parent) return null;
|
|
14844
|
+
const parentText = _substituteStructuralGenerics(parent, bindings);
|
|
14845
|
+
const parsedParent = _structuralTypeExpression(parentText);
|
|
14846
|
+
if (!parsedParent) return null;
|
|
14847
|
+
const parentOrigin = _resolveFlowTypeOrigin(
|
|
14848
|
+
index, definition.file, parsedParent.head);
|
|
14849
|
+
if (!parentOrigin?.fromFile) return null;
|
|
14850
|
+
return _structuralFieldTypeExpression(
|
|
14851
|
+
index, parentText, parentOrigin.fromFile, fieldName, depth + 1);
|
|
14852
|
+
}
|
|
14853
|
+
|
|
14854
|
+
function _structuralReturnedReceiverType(index, receiverText, fromFile, fieldNames) {
|
|
14855
|
+
let current = { text: receiverText, fromFile };
|
|
14856
|
+
for (const fieldName of fieldNames) {
|
|
14857
|
+
current = _structuralFieldTypeExpression(
|
|
14858
|
+
index, current.text, current.fromFile, fieldName);
|
|
14859
|
+
if (!current) return null;
|
|
14860
|
+
}
|
|
14861
|
+
return current;
|
|
14862
|
+
}
|
|
14863
|
+
|
|
13929
14864
|
/**
|
|
13930
14865
|
* Resolve method `methodName` on type `typeName` (identity-pinned to
|
|
13931
14866
|
* `fromFile` when known) and return its resolved return-type head as
|
|
@@ -13950,8 +14885,16 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
|
|
|
13950
14885
|
const typeDefs = (index.symbols.get(typeName) || []).filter(d => _FOLD_TYPE_KINDS.has(d.type));
|
|
13951
14886
|
if (typeDefs.length > 1 && owned.length > 0) {
|
|
13952
14887
|
if (!fromFile) return null;
|
|
13953
|
-
|
|
13954
|
-
|
|
14888
|
+
if (nominal) {
|
|
14889
|
+
const dir = path.dirname(fromFile);
|
|
14890
|
+
owned = owned.filter(d => d.file === fromFile ||
|
|
14891
|
+
(d.file && path.dirname(d.file) === dir));
|
|
14892
|
+
} else {
|
|
14893
|
+
// Structural sibling modules are distinct identities. Directory
|
|
14894
|
+
// co-location is package evidence for Go/Rust/Java impl layouts,
|
|
14895
|
+
// but never merges two Python/JS/TS classes with the same name.
|
|
14896
|
+
owned = owned.filter(d => d.file === fromFile);
|
|
14897
|
+
}
|
|
13955
14898
|
}
|
|
13956
14899
|
if (owned.length === 0) {
|
|
13957
14900
|
// Inheritance walk: resolve on a declared ancestor; Self/this still
|
|
@@ -13983,16 +14926,46 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
|
|
|
13983
14926
|
// Structural: heads must agree; `this`/`Self` are the receiver's type
|
|
13984
14927
|
// (checked BEFORE the reject set — with a known owner they ARE identity);
|
|
13985
14928
|
// un-awaited async producers stay untyped (the value is a coroutine).
|
|
13986
|
-
|
|
14929
|
+
// TypeScript/Python overload signatures are the public call contract;
|
|
14930
|
+
// their runtime implementation may intentionally omit a return annotation.
|
|
14931
|
+
// Once a compiler-recognized signature group exists, use only those
|
|
14932
|
+
// declarations for result-flow agreement. Conflicting signature heads
|
|
14933
|
+
// still abstain below (fix #316, zod-measured fluent default chains).
|
|
14934
|
+
const contracts = owned.some(d => d.isSignature)
|
|
14935
|
+
? owned.filter(d => d.isSignature) : owned;
|
|
14936
|
+
if (language === 'python' && !opts.consumerAwaited && contracts.some(d => d.isAsync)) return null;
|
|
14937
|
+
const returnedPaths = contracts.map(definition => definition.returnedReceiverPath);
|
|
14938
|
+
if (contracts.length > 0 && returnedPaths.every(path =>
|
|
14939
|
+
Array.isArray(path) && path.length > 0) &&
|
|
14940
|
+
new Set(returnedPaths.map(path => path.join('\0'))).size === 1) {
|
|
14941
|
+
const receiverText = opts.selfTypeText || selfType;
|
|
14942
|
+
const resolved = _structuralReturnedReceiverType(
|
|
14943
|
+
index, receiverText, fromFile || contracts[0].file, returnedPaths[0]);
|
|
14944
|
+
const parsed = resolved && _structuralTypeExpression(resolved.text);
|
|
14945
|
+
if (!parsed || /^[A-Z][A-Z0-9]?$/.test(parsed.head) ||
|
|
14946
|
+
_STRUCTURAL_FLOW_REJECT.has(parsed.head)) return null;
|
|
14947
|
+
const origin = _resolveFlowTypeOrigin(
|
|
14948
|
+
index, resolved.fromFile || contracts[0].file, parsed.head);
|
|
14949
|
+
if (!origin?.fromFile) return null;
|
|
14950
|
+
return {
|
|
14951
|
+
type: parsed.head,
|
|
14952
|
+
typeText: resolved.text,
|
|
14953
|
+
fromFile: origin.fromFile,
|
|
14954
|
+
};
|
|
14955
|
+
}
|
|
13987
14956
|
const heads = new Set();
|
|
13988
|
-
|
|
14957
|
+
const typeTexts = new Set();
|
|
14958
|
+
for (const d of contracts) {
|
|
13989
14959
|
if (!d.returnType) return null;
|
|
13990
|
-
|
|
14960
|
+
const returnText = String(d.returnType).replace(
|
|
14961
|
+
/\b(?:this|Self)\b/g, opts.selfTypeText || selfType);
|
|
14962
|
+
let h = _structuralTypeHead(returnText, {
|
|
13991
14963
|
unwrapAsync: opts.consumerAwaited, index, language, originFile: d.file,
|
|
13992
14964
|
});
|
|
13993
14965
|
if (h === 'this' || h === 'Self') h = selfType;
|
|
13994
14966
|
if (!h) return null;
|
|
13995
14967
|
heads.add(h);
|
|
14968
|
+
typeTexts.add(returnText);
|
|
13996
14969
|
if (heads.size > 1) return null;
|
|
13997
14970
|
}
|
|
13998
14971
|
const head = [...heads][0];
|
|
@@ -14001,16 +14974,23 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
|
|
|
14001
14974
|
const returnTypeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
14002
14975
|
if (returnTypeDefs.length > 0) {
|
|
14003
14976
|
const origins = new Set();
|
|
14004
|
-
for (const d of
|
|
14977
|
+
for (const d of contracts) {
|
|
14005
14978
|
const origin = _resolveFlowTypeOrigin(index, d.file || opts.filePath, head);
|
|
14006
14979
|
if (!origin) return null;
|
|
14007
14980
|
origins.add(origin.fromFile);
|
|
14008
14981
|
if (origins.size > 1) return null;
|
|
14009
14982
|
}
|
|
14010
14983
|
const fromFile = [...origins][0];
|
|
14011
|
-
return {
|
|
14984
|
+
return {
|
|
14985
|
+
type: head,
|
|
14986
|
+
...(typeTexts.size === 1 && { typeText: [...typeTexts][0] }),
|
|
14987
|
+
...(fromFile && { fromFile }),
|
|
14988
|
+
};
|
|
14012
14989
|
}
|
|
14013
|
-
return {
|
|
14990
|
+
return {
|
|
14991
|
+
type: head,
|
|
14992
|
+
...(typeTexts.size === 1 && { typeText: [...typeTexts][0] }),
|
|
14993
|
+
};
|
|
14014
14994
|
}
|
|
14015
14995
|
|
|
14016
14996
|
function _rustMacroDefinitions(index, fileEntry, filePath, record) {
|
|
@@ -14495,6 +15475,26 @@ function _typeOfCallResultFold(index, fileEntry, filePath, record, ctx, consumer
|
|
|
14495
15475
|
return out;
|
|
14496
15476
|
}
|
|
14497
15477
|
|
|
15478
|
+
function _returnedCallRecord(ctx, start, end, current) {
|
|
15479
|
+
if (!ctx.returnedCallIndex) {
|
|
15480
|
+
ctx.returnedCallIndex = new Map();
|
|
15481
|
+
const records = ctx.allRecords || ctx.records || [];
|
|
15482
|
+
for (const candidate of records) {
|
|
15483
|
+
if (candidate.callStart == null || candidate.callEnd == null) continue;
|
|
15484
|
+
const key = `${candidate.callStart}:${candidate.callEnd}`;
|
|
15485
|
+
let group = ctx.returnedCallIndex.get(key);
|
|
15486
|
+
if (!group) {
|
|
15487
|
+
group = [];
|
|
15488
|
+
ctx.returnedCallIndex.set(key, group);
|
|
15489
|
+
}
|
|
15490
|
+
group.push(candidate);
|
|
15491
|
+
}
|
|
15492
|
+
}
|
|
15493
|
+
const matches = (ctx.returnedCallIndex.get(`${start}:${end}`) || [])
|
|
15494
|
+
.filter(candidate => candidate !== current);
|
|
15495
|
+
return matches.length === 1 ? matches[0] : null;
|
|
15496
|
+
}
|
|
15497
|
+
|
|
14498
15498
|
function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, consumerAwaited) {
|
|
14499
15499
|
const language = fileEntry.language;
|
|
14500
15500
|
const traits = langTraits(language);
|
|
@@ -14505,6 +15505,27 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14505
15505
|
return _rustMacroCallResultType(index, fileEntry, filePath, record);
|
|
14506
15506
|
}
|
|
14507
15507
|
|
|
15508
|
+
// Structural constructor expression: `Environment().getattr(...)`.
|
|
15509
|
+
// Python/JS class calls are plain call records rather than nominal
|
|
15510
|
+
// constructor records. A unique in-scope class declaration is exact
|
|
15511
|
+
// result-type evidence; a same-named callable or unresolved origin keeps
|
|
15512
|
+
// the chain untyped. This is deliberately identity-pinned through the
|
|
15513
|
+
// calling file's imports, not a project-wide capitalization guess.
|
|
15514
|
+
if (!nominal && !record.isMethod && !record.receiver) {
|
|
15515
|
+
const named = index.symbols.get(name) || [];
|
|
15516
|
+
const typeDefs = named.filter(definition =>
|
|
15517
|
+
IDENTITY_TYPE_KINDS.has(definition.type) && definition.file);
|
|
15518
|
+
const callableDefs = named.filter(definition =>
|
|
15519
|
+
!NON_CALLABLE_TYPES.has(definition.type));
|
|
15520
|
+
if (typeDefs.length === 1 && callableDefs.length === 0) {
|
|
15521
|
+
const origin = _resolveFlowTypeOrigin(index, filePath, name);
|
|
15522
|
+
if (origin?.fromFile === typeDefs[0].file) {
|
|
15523
|
+
return { type: name, fromFile: origin.fromFile,
|
|
15524
|
+
exactConstructor: true };
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
}
|
|
15528
|
+
|
|
14508
15529
|
// Path producer (Rust): Command::new(...) — the last path segment names
|
|
14509
15530
|
// the impl type (flow-map rails: module-path producers stay untyped);
|
|
14510
15531
|
// Self::new() resolves through the enclosing impl. The type's identity
|
|
@@ -14569,8 +15590,15 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14569
15590
|
if (!nominal && record.isMethod &&
|
|
14570
15591
|
(record.receiver || record.receiverModuleSpecifier) &&
|
|
14571
15592
|
(record.receiverIsModule || _isStructuralImportReceiver(fileEntry, record))) {
|
|
14572
|
-
const
|
|
14573
|
-
|
|
15593
|
+
const composite = record.receiverModuleComposition
|
|
15594
|
+
? _structuralCompositeModuleOwnership(index, fileEntry, record)
|
|
15595
|
+
: null;
|
|
15596
|
+
if (composite && composite.verdict !== 'yes') return null;
|
|
15597
|
+
const binding = composite?.binding ||
|
|
15598
|
+
_structuralModuleBindings(fileEntry, record)[0];
|
|
15599
|
+
const rel = composite?.rel ||
|
|
15600
|
+
(binding && fileEntry.moduleResolved &&
|
|
15601
|
+
fileEntry.moduleResolved[binding.module]);
|
|
14574
15602
|
if (binding && !rel) {
|
|
14575
15603
|
const mod = String(binding.module);
|
|
14576
15604
|
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
@@ -14629,7 +15657,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14629
15657
|
return { type: head, ...(fromFile && { fromFile }) };
|
|
14630
15658
|
}
|
|
14631
15659
|
// self/this/cls receiver: resolve through the enclosing class (+ walk).
|
|
14632
|
-
if (record.isMethod &&
|
|
15660
|
+
if (record.isMethod && !record.receiverField &&
|
|
15661
|
+
['self', 'this', 'cls'].includes(record.receiver)) {
|
|
14633
15662
|
const enclosing = index.findEnclosingFunction(filePath, record.line, true);
|
|
14634
15663
|
let cls = enclosing && enclosing.className;
|
|
14635
15664
|
let ctxFile = filePath;
|
|
@@ -14668,6 +15697,12 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14668
15697
|
if (!rt && record.receiverField && record.receiverRoot) {
|
|
14669
15698
|
let rootType = record.receiverRootType;
|
|
14670
15699
|
let rootFromFile;
|
|
15700
|
+
if (!rootType && language === 'python' &&
|
|
15701
|
+
['self', 'cls'].includes(record.receiverRoot)) {
|
|
15702
|
+
rootType = index.findEnclosingFunction(
|
|
15703
|
+
filePath, record.line, true)?.className;
|
|
15704
|
+
if (rootType) rootFromFile = filePath;
|
|
15705
|
+
}
|
|
14671
15706
|
if (!rootType) {
|
|
14672
15707
|
const flowMap = ctx.getFlowMap();
|
|
14673
15708
|
const rootFlow = flowMap && _lookupReturnTypeFlow(flowMap, {
|
|
@@ -14705,6 +15740,19 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14705
15740
|
};
|
|
14706
15741
|
}
|
|
14707
15742
|
}
|
|
15743
|
+
if (!rt && langTraits(language)?.typeSystem === 'structural') {
|
|
15744
|
+
const moduleValueInfo = {};
|
|
15745
|
+
const moduleValueType = _structuralModuleValueFieldType(
|
|
15746
|
+
index, fileEntry, record, moduleValueInfo);
|
|
15747
|
+
if (moduleValueType) {
|
|
15748
|
+
rt = {
|
|
15749
|
+
type: moduleValueType,
|
|
15750
|
+
...(moduleValueInfo.fromFile && {
|
|
15751
|
+
fromFile: moduleValueInfo.fromFile,
|
|
15752
|
+
}),
|
|
15753
|
+
};
|
|
15754
|
+
}
|
|
15755
|
+
}
|
|
14708
15756
|
}
|
|
14709
15757
|
if (!rt && record.receiverCall && (!record.receiver || record.receiverIsChainRoot)) {
|
|
14710
15758
|
rt = _foldChainedReceiverType(index, fileEntry, filePath, record, ctx);
|
|
@@ -14733,7 +15781,7 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14733
15781
|
if (builtinReturn) return { type: builtinReturn };
|
|
14734
15782
|
}
|
|
14735
15783
|
return _methodReturnOnType(index, rt.type, rt.fromFile, name, language,
|
|
14736
|
-
{ filePath, consumerAwaited });
|
|
15784
|
+
{ filePath, consumerAwaited, selfTypeText: rt.typeText });
|
|
14737
15785
|
}
|
|
14738
15786
|
if (nominal) return null;
|
|
14739
15787
|
// One-hop agreement (the #207/#219 discipline, one level deeper):
|
|
@@ -14801,7 +15849,24 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
14801
15849
|
const sameFile = defs.filter(d => d.file === filePath);
|
|
14802
15850
|
if (sameFile.length === 1) chosen = sameFile[0];
|
|
14803
15851
|
}
|
|
14804
|
-
if (!chosen
|
|
15852
|
+
if (!chosen) return null;
|
|
15853
|
+
// An expression-bodied arrow returns its expression by construction.
|
|
15854
|
+
// The parser persists the exact span only when that expression IS a call;
|
|
15855
|
+
// resolve that existing call record through the normal compiler-evidence
|
|
15856
|
+
// rails. Async functions expose the inner value only when the consumer
|
|
15857
|
+
// awaits them; generators are never ordinary value factories (fix #317,
|
|
15858
|
+
// zod-measured local `base = () => z.object(...)` flow).
|
|
15859
|
+
if (!nominal && !chosen.returnType && !chosen.isGenerator &&
|
|
15860
|
+
(!chosen.isAsync || consumerAwaited) &&
|
|
15861
|
+
chosen.returnedCallStart != null && chosen.returnedCallEnd != null) {
|
|
15862
|
+
const returned = _returnedCallRecord(
|
|
15863
|
+
ctx, chosen.returnedCallStart, chosen.returnedCallEnd, record);
|
|
15864
|
+
if (returned) {
|
|
15865
|
+
return _typeOfCallResultFold(
|
|
15866
|
+
index, fileEntry, filePath, returned, ctx, consumerAwaited);
|
|
15867
|
+
}
|
|
15868
|
+
}
|
|
15869
|
+
if (!chosen.returnType) return null;
|
|
14805
15870
|
if (nominal) {
|
|
14806
15871
|
if (language === 'cpp') {
|
|
14807
15872
|
const concrete = _cppAutoReturnConcreteType(index, chosen);
|
|
@@ -14896,7 +15961,8 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
|
|
|
14896
15961
|
// fallback is not allowed to borrow Mocker.number (or another
|
|
14897
15962
|
// module/version) as its return type. Keep the consumer untyped and
|
|
14898
15963
|
// visible instead of manufacturing exclusion-grade evidence.
|
|
14899
|
-
if (prods.some(r => r.receiverIsModule ||
|
|
15964
|
+
if (prods.some(r => r.receiverIsModule || r.receiverModuleComposition ||
|
|
15965
|
+
_isStructuralImportReceiver(fileEntry, r))) {
|
|
14900
15966
|
return { suppressFallback: true };
|
|
14901
15967
|
}
|
|
14902
15968
|
return null;
|
|
@@ -14910,11 +15976,18 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
|
|
|
14910
15976
|
if (results.some(r => r.externalVia)) return null;
|
|
14911
15977
|
if (new Set(results.map(r => r.type)).size !== 1) return null;
|
|
14912
15978
|
const fromFiles = new Set(results.map(r => r.fromFile));
|
|
15979
|
+
const typeTexts = new Set(results.map(r => r.typeText));
|
|
14913
15980
|
let result = {
|
|
14914
15981
|
type: results[0].type,
|
|
15982
|
+
...(typeTexts.size === 1 && results[0].typeText && {
|
|
15983
|
+
typeText: results[0].typeText,
|
|
15984
|
+
}),
|
|
14915
15985
|
...(fromFiles.size === 1 && results[0].fromFile && {
|
|
14916
15986
|
fromFile: results[0].fromFile,
|
|
14917
15987
|
}),
|
|
15988
|
+
...(results.every(r => r.exactConstructor) && {
|
|
15989
|
+
exactConstructor: true,
|
|
15990
|
+
}),
|
|
14918
15991
|
};
|
|
14919
15992
|
if (call.receiverFields?.length) {
|
|
14920
15993
|
const fieldInfo = {};
|
|
@@ -14944,6 +16017,7 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
|
|
|
14944
16017
|
// return annotation from an unrelated class. Capitalized named imports remain
|
|
14945
16018
|
// eligible for class/static-method resolution.
|
|
14946
16019
|
function _isStructuralImportReceiver(fileEntry, record) {
|
|
16020
|
+
if (record?.receiverModuleSpecifier || record?.receiverModuleComposition) return true;
|
|
14947
16021
|
if (!record?.receiver || !/^[a-z_$]/.test(record.receiver)) return false;
|
|
14948
16022
|
return (fileEntry?.importBindings || []).some(b => b.name === record.receiver);
|
|
14949
16023
|
}
|