gitnexus 1.6.2-rc.20 → 1.6.2-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shared/mro-strategy.d.ts +38 -16
- package/dist/_shared/mro-strategy.d.ts.map +1 -1
- package/dist/core/ingestion/call-processor.d.ts +1 -1
- package/dist/core/ingestion/call-processor.js +172 -42
- package/dist/core/ingestion/call-routing.d.ts +8 -12
- package/dist/core/ingestion/call-routing.js +13 -34
- package/dist/core/ingestion/call-types.d.ts +75 -0
- package/dist/core/ingestion/heritage-extractors/configs/go.d.ts +13 -0
- package/dist/core/ingestion/heritage-extractors/configs/go.js +20 -0
- package/dist/core/ingestion/heritage-extractors/configs/ruby.d.ts +18 -0
- package/dist/core/ingestion/heritage-extractors/configs/ruby.js +65 -0
- package/dist/core/ingestion/heritage-extractors/generic.d.ts +23 -0
- package/dist/core/ingestion/heritage-extractors/generic.js +47 -0
- package/dist/core/ingestion/heritage-processor.d.ts +9 -0
- package/dist/core/ingestion/heritage-processor.js +120 -85
- package/dist/core/ingestion/heritage-types.d.ts +73 -0
- package/dist/core/ingestion/heritage-types.js +2 -0
- package/dist/core/ingestion/import-resolvers/configs/python.js +14 -0
- package/dist/core/ingestion/language-provider.d.ts +69 -1
- package/dist/core/ingestion/languages/c-cpp.js +3 -0
- package/dist/core/ingestion/languages/csharp.js +2 -0
- package/dist/core/ingestion/languages/dart.js +2 -0
- package/dist/core/ingestion/languages/go.js +3 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/languages/php.js +2 -0
- package/dist/core/ingestion/languages/python.js +2 -0
- package/dist/core/ingestion/languages/ruby.js +92 -15
- package/dist/core/ingestion/languages/rust.js +2 -0
- package/dist/core/ingestion/languages/swift.js +2 -0
- package/dist/core/ingestion/languages/typescript.js +3 -0
- package/dist/core/ingestion/languages/vue.js +2 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +35 -0
- package/dist/core/ingestion/model/heritage-map.js +110 -9
- package/dist/core/ingestion/model/resolve.d.ts +30 -28
- package/dist/core/ingestion/model/resolve.js +105 -25
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +9 -3
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +7 -0
- package/dist/core/ingestion/pipeline.d.ts +11 -0
- package/dist/core/ingestion/pipeline.js +9 -2
- package/dist/core/ingestion/utils/ast-helpers.js +19 -2
- package/dist/core/ingestion/utils/ruby-self-call.d.ts +52 -0
- package/dist/core/ingestion/utils/ruby-self-call.js +59 -0
- package/dist/core/ingestion/workers/parse-worker.js +57 -60
- package/dist/types/pipeline.d.ts +6 -0
- package/package.json +1 -1
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MRO (Method Resolution Order) strategy — shared
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
2
|
+
* MRO (Method Resolution Order) strategy — shared canonical definition.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `gitnexus-shared` so `model/resolve.ts` and `mro-processor.ts` share
|
|
5
|
+
* the type without importing the language registry (avoids circular coupling).
|
|
6
|
+
*
|
|
7
|
+
* `first-wins` (default, Java/C#/Kotlin/Go/Swift/Dart):
|
|
8
|
+
* BFS ancestor walk in declaration order; first match wins.
|
|
9
|
+
*
|
|
10
|
+
* `leftmost-base` (C++):
|
|
11
|
+
* BFS walk; HeritageMap preserves source insertion order, so BFS naturally
|
|
12
|
+
* picks the leftmost base in diamond inheritance.
|
|
13
|
+
*
|
|
14
|
+
* `c3` (Python):
|
|
15
|
+
* C3-linearization; falls back to BFS on cyclic/inconsistent hierarchy.
|
|
16
|
+
* See model/resolve.ts § c3Linearize.
|
|
17
|
+
*
|
|
18
|
+
* `implements-split` (Java/C#/Kotlin):
|
|
19
|
+
* Low-level lookup is BFS; graph-level mro-processor detects and warns on
|
|
20
|
+
* interface-default method ambiguity.
|
|
21
|
+
*
|
|
22
|
+
* `qualified-syntax` (Rust):
|
|
23
|
+
* No auto-resolution — `lookupMethodByOwnerWithMRO` returns undefined immediately.
|
|
24
|
+
* Rust requires explicit `<Type as Trait>::method` syntax.
|
|
25
|
+
*
|
|
26
|
+
* `ruby-mixin` (Ruby):
|
|
27
|
+
* Kind-aware walk that does NOT short-circuit on direct owner first (`prepend`
|
|
28
|
+
* must beat the class's own method). Walk order:
|
|
29
|
+
* 1. Prepend providers (reverse declaration — last-prepended wins)
|
|
30
|
+
* 2. Direct owner's own methods
|
|
31
|
+
* 3. Include providers (reverse declaration)
|
|
32
|
+
* 4. Transitive ancestors (BFS fallback)
|
|
33
|
+
* Singleton dispatch: caller passes `ancestryOverride` (extend providers only);
|
|
34
|
+
* becomes a simple left-to-right scan. Miss NEVER falls through to file-scoped
|
|
35
|
+
* lookup — null-routes or honors `fallback`.
|
|
36
|
+
*
|
|
37
|
+
* @see model/resolve.ts § lookupMethodByOwnerWithMRO
|
|
38
|
+
* @see languages/ruby.ts § selectDispatch
|
|
17
39
|
*/
|
|
18
|
-
export type MroStrategy = 'first-wins' | 'c3' | 'leftmost-base' | 'implements-split' | 'qualified-syntax';
|
|
40
|
+
export type MroStrategy = 'first-wins' | 'c3' | 'leftmost-base' | 'implements-split' | 'qualified-syntax' | 'ruby-mixin';
|
|
19
41
|
//# sourceMappingURL=mro-strategy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mro-strategy.d.ts","sourceRoot":"","sources":["../src/mro-strategy.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mro-strategy.d.ts","sourceRoot":"","sources":["../src/mro-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,IAAI,GACJ,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,YAAY,CAAC"}
|
|
@@ -123,7 +123,7 @@ export declare const _resolveCallTargetForTesting: (call: Pick<ExtractedCall, "c
|
|
|
123
123
|
* @param ctx - Resolution context
|
|
124
124
|
* @param heritageMap - Optional heritage map for MRO-aware ancestor walking
|
|
125
125
|
*/
|
|
126
|
-
export declare const resolveMemberCall: (ownerType: string, methodName: string, currentFile: string, ctx: ResolutionContext, heritageMap?: HeritageMap, argCount?: number) => ResolveResult | null;
|
|
126
|
+
export declare const resolveMemberCall: (ownerType: string, methodName: string, currentFile: string, ctx: ResolutionContext, heritageMap?: HeritageMap, argCount?: number, ancestryView?: "instance" | "singleton") => ResolveResult | null;
|
|
127
127
|
/**
|
|
128
128
|
* Resolve a free-function call using `lookupExact` (same-file) + import-scoped
|
|
129
129
|
* resolution via `ctx.resolve()`.
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { CLASS_TYPES, CALL_TARGET_TYPES, lookupMethodByOwnerWithMRO } from './model/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* DAG stage 4 fallback: used when `selectDispatch` is absent or returns null.
|
|
4
|
+
* Preserves pre-DAG dispatch semantics:
|
|
5
|
+
* - 'constructor' → constructor branch
|
|
6
|
+
* - 'free' → free branch (admits Swift/Kotlin class-target fast path)
|
|
7
|
+
* - 'member' or undefined → owner-scoped branch
|
|
8
|
+
*
|
|
9
|
+
* `undefined` callForm MUST route through owner-scoped (not free) so bare
|
|
10
|
+
* identifiers without a classified shape do NOT trigger `resolveFreeCall`'s
|
|
11
|
+
* class-target fast path. Without a `receiverTypeName`, the owner-scoped
|
|
12
|
+
* branch falls through to `resolveModuleAliasedCall` + `singleCandidate`,
|
|
13
|
+
* matching legacy behavior where non-callable symbols (Class, Interface)
|
|
14
|
+
* null-route instead of producing spurious Constructor edges.
|
|
15
|
+
*/
|
|
16
|
+
const defaultDispatchDecision = (callForm) => {
|
|
17
|
+
if (callForm === 'constructor')
|
|
18
|
+
return { primary: 'constructor' };
|
|
19
|
+
if (callForm === 'free')
|
|
20
|
+
return { primary: 'free' };
|
|
21
|
+
return { primary: 'owner-scoped' };
|
|
22
|
+
};
|
|
2
23
|
import Parser from 'tree-sitter';
|
|
3
24
|
import { TIER_CONFIDENCE } from './model/resolution-context.js';
|
|
4
25
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
@@ -609,23 +630,27 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
609
630
|
// Extract heritage from query matches to build parentMap for buildTypeEnv.
|
|
610
631
|
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
|
611
632
|
const fileParentMap = new Map();
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
633
|
+
if (provider.heritageExtractor) {
|
|
634
|
+
for (const match of matches) {
|
|
635
|
+
const captureMap = {};
|
|
636
|
+
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
|
637
|
+
if (captureMap['heritage.class']) {
|
|
638
|
+
const heritageItems = provider.heritageExtractor.extract(captureMap, {
|
|
639
|
+
filePath: file.path,
|
|
640
|
+
language,
|
|
641
|
+
});
|
|
642
|
+
for (const item of heritageItems) {
|
|
643
|
+
if (item.kind === 'extends') {
|
|
644
|
+
let parents = fileParentMap.get(item.className);
|
|
645
|
+
if (!parents) {
|
|
646
|
+
parents = [];
|
|
647
|
+
fileParentMap.set(item.className, parents);
|
|
648
|
+
}
|
|
649
|
+
if (!parents.includes(item.parentName))
|
|
650
|
+
parents.push(item.parentName);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
626
653
|
}
|
|
627
|
-
if (!parents.includes(parentName))
|
|
628
|
-
parents.push(parentName);
|
|
629
654
|
}
|
|
630
655
|
}
|
|
631
656
|
const parentMap = fileParentMap;
|
|
@@ -786,22 +811,29 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
786
811
|
if (!nameNode)
|
|
787
812
|
return;
|
|
788
813
|
const calledName = nameNode.text;
|
|
814
|
+
// Check heritage extractor for call-based heritage (e.g., Ruby include/extend/prepend)
|
|
815
|
+
if (provider.heritageExtractor?.extractFromCall) {
|
|
816
|
+
const heritageItems = provider.heritageExtractor.extractFromCall(calledName, captureMap['call'], { filePath: file.path, language });
|
|
817
|
+
if (heritageItems !== null) {
|
|
818
|
+
for (const item of heritageItems) {
|
|
819
|
+
collectedHeritage.push({
|
|
820
|
+
filePath: file.path,
|
|
821
|
+
className: item.className,
|
|
822
|
+
parentName: item.parentName,
|
|
823
|
+
kind: item.kind,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Dispatch: route language-specific calls (properties, imports)
|
|
830
|
+
// Heritage routing is handled by heritageExtractor.extractFromCall above.
|
|
789
831
|
const routed = callRouter?.(calledName, captureMap['call']);
|
|
790
832
|
if (routed) {
|
|
791
833
|
switch (routed.kind) {
|
|
792
834
|
case 'skip':
|
|
793
835
|
case 'import':
|
|
794
836
|
return;
|
|
795
|
-
case 'heritage':
|
|
796
|
-
for (const item of routed.items) {
|
|
797
|
-
collectedHeritage.push({
|
|
798
|
-
filePath: file.path,
|
|
799
|
-
className: item.enclosingClass,
|
|
800
|
-
parentName: item.mixinName,
|
|
801
|
-
kind: item.heritageKind,
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
return;
|
|
805
837
|
case 'properties': {
|
|
806
838
|
const fileId = generateId('File', file.path);
|
|
807
839
|
const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
|
|
@@ -852,9 +884,16 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
852
884
|
}
|
|
853
885
|
if (provider.isBuiltInName(calledName))
|
|
854
886
|
return;
|
|
855
|
-
|
|
856
|
-
|
|
887
|
+
// --- DAG stage 2-3: classify-form + infer-receiver (shared defaults) ---
|
|
888
|
+
// These stages run the shared inference chain. Language providers can
|
|
889
|
+
// customize infer-receiver (stage 3) via the inferImplicitReceiver hook
|
|
890
|
+
// which runs AFTER this default chain (typed-binding → constructor-map →
|
|
891
|
+
// module-alias → class-as-receiver → mixed-chain), and selectDispatch
|
|
892
|
+
// (stage 4) which picks the resolver branch.
|
|
893
|
+
let callForm = inferCallForm(callNode, nameNode);
|
|
894
|
+
let receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
857
895
|
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
896
|
+
let receiverSource = receiverTypeName ? 'typed-binding' : 'none';
|
|
858
897
|
// Phase P: virtual dispatch override — when the declared type is a base class but
|
|
859
898
|
// the constructor created a known subclass, prefer the more specific type.
|
|
860
899
|
// Checks per-file parentMap first, then falls back to globalParentMap for
|
|
@@ -890,6 +929,7 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
890
929
|
(ctx.model.types.lookupClassByName(ctorType).length > 0 &&
|
|
891
930
|
ctx.model.types.lookupClassByName(receiverTypeName).length > 0)) {
|
|
892
931
|
receiverTypeName = ctorType;
|
|
932
|
+
receiverSource = 'constructor-map';
|
|
893
933
|
}
|
|
894
934
|
}
|
|
895
935
|
}
|
|
@@ -898,18 +938,25 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
898
938
|
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider);
|
|
899
939
|
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
900
940
|
receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
|
|
941
|
+
if (receiverTypeName)
|
|
942
|
+
receiverSource = 'constructor-map';
|
|
901
943
|
}
|
|
902
|
-
// Fall back to class-as-receiver for static method calls (e.g. UserService.find_user()
|
|
903
|
-
// When the receiver name is not a variable in TypeEnv but
|
|
904
|
-
//
|
|
944
|
+
// Fall back to class-as-receiver for static method calls (e.g. UserService.find_user(),
|
|
945
|
+
// Greetable.format()). When the receiver name is not a variable in TypeEnv but
|
|
946
|
+
// resolves to a class-like symbol (Class / Interface / Struct / Enum / Trait) via
|
|
947
|
+
// tiered resolution, use it directly as the receiver type. `Trait` is included so
|
|
948
|
+
// Ruby module class-method calls flow through the class-as-receiver path and reach
|
|
949
|
+
// the `selectDispatch` hook's singleton branch.
|
|
905
950
|
if (!receiverTypeName && receiverName && callForm === 'member') {
|
|
906
951
|
const typeResolved = ctx.resolve(receiverName, file.path);
|
|
907
952
|
if (typeResolved &&
|
|
908
953
|
typeResolved.candidates.some((d) => d.type === 'Class' ||
|
|
909
954
|
d.type === 'Interface' ||
|
|
910
955
|
d.type === 'Struct' ||
|
|
911
|
-
d.type === 'Enum'
|
|
956
|
+
d.type === 'Enum' ||
|
|
957
|
+
d.type === 'Trait')) {
|
|
912
958
|
receiverTypeName = receiverName;
|
|
959
|
+
receiverSource = 'class-as-receiver';
|
|
913
960
|
}
|
|
914
961
|
}
|
|
915
962
|
// Hoist sourceId so it's available for ACCESSES edge emission during chain walk.
|
|
@@ -941,10 +988,48 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
941
988
|
}
|
|
942
989
|
if (currentType) {
|
|
943
990
|
receiverTypeName = walkMixedChain(extracted.chain, currentType, file.path, ctx, makeAccessEmitter(graph, sourceId), heritageMap);
|
|
991
|
+
if (receiverTypeName)
|
|
992
|
+
receiverSource = 'mixed-chain';
|
|
944
993
|
}
|
|
945
994
|
}
|
|
946
995
|
}
|
|
947
996
|
}
|
|
997
|
+
// --- DAG stage 3: infer-receiver (provider hook) ---
|
|
998
|
+
// Synthesize implicit receivers for languages that omit them (e.g., Ruby bare-call).
|
|
999
|
+
// This hook runs AFTER the shared inference chain so explicit receivers /
|
|
1000
|
+
// typed bindings always take precedence. Output (if non-null) overlays onto
|
|
1001
|
+
// the ReceiverEnriched for the next stage.
|
|
1002
|
+
let dispatchHint;
|
|
1003
|
+
if (provider.inferImplicitReceiver) {
|
|
1004
|
+
const override = provider.inferImplicitReceiver({
|
|
1005
|
+
calledName,
|
|
1006
|
+
callForm,
|
|
1007
|
+
receiverName,
|
|
1008
|
+
receiverTypeName,
|
|
1009
|
+
callNode,
|
|
1010
|
+
filePath: file.path,
|
|
1011
|
+
});
|
|
1012
|
+
if (override) {
|
|
1013
|
+
callForm = override.callForm;
|
|
1014
|
+
receiverName = override.receiverName;
|
|
1015
|
+
receiverTypeName = override.receiverTypeName;
|
|
1016
|
+
receiverSource = override.receiverSource;
|
|
1017
|
+
dispatchHint = override.hint;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// --- DAG stage 4: select-dispatch (provider hook + default fallback) ---
|
|
1021
|
+
// Decide which resolver path to try first (primary) and fallback strategy.
|
|
1022
|
+
// Language providers can customize dispatch via selectDispatch hook; all
|
|
1023
|
+
// others use the shared defaultDispatchDecision. Always non-null after this
|
|
1024
|
+
// block so downstream resolvers are table-driven.
|
|
1025
|
+
const dispatchDecision = provider.selectDispatch?.({
|
|
1026
|
+
calledName,
|
|
1027
|
+
callForm,
|
|
1028
|
+
receiverName,
|
|
1029
|
+
receiverTypeName,
|
|
1030
|
+
receiverSource,
|
|
1031
|
+
hint: dispatchHint,
|
|
1032
|
+
}) ?? defaultDispatchDecision(callForm);
|
|
948
1033
|
// Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
|
|
949
1034
|
// Only used when multiple candidates survive arity filtering — ~1-3% of calls.
|
|
950
1035
|
const langConfig = provider.typeConfig;
|
|
@@ -957,7 +1042,7 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
957
1042
|
callForm,
|
|
958
1043
|
receiverTypeName,
|
|
959
1044
|
receiverName,
|
|
960
|
-
}, file.path, ctx, hints, widenCache, undefined, heritageMap);
|
|
1045
|
+
}, file.path, ctx, hints, widenCache, undefined, heritageMap, dispatchDecision);
|
|
961
1046
|
if (!resolved)
|
|
962
1047
|
return;
|
|
963
1048
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
@@ -1359,26 +1444,45 @@ const singleCandidate = (tiered, argCount, callForm) => {
|
|
|
1359
1444
|
};
|
|
1360
1445
|
/** @internal Exported for unit tests. Do not use outside tests. */
|
|
1361
1446
|
export const _resolveCallTargetForTesting = (call, currentFile, ctx, opts) => resolveCallTarget(call, currentFile, ctx, opts?.overloadHints, opts?.widenCache, opts?.preComputedArgTypes, opts?.heritageMap);
|
|
1362
|
-
const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes, heritageMap) => {
|
|
1447
|
+
const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, preComputedArgTypes, heritageMap, dispatchDecision) => {
|
|
1363
1448
|
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
1364
1449
|
if (!tiered)
|
|
1365
1450
|
return null;
|
|
1366
|
-
|
|
1451
|
+
// DAG dispatch: use decision.primary to pick the resolver branch.
|
|
1452
|
+
// Callers that own the DAG (processCalls + crossFile deferred paths)
|
|
1453
|
+
// pass a decision; other callers use the shared default ladder.
|
|
1454
|
+
// Language-specific primary / fallback / ancestryView overrides come from
|
|
1455
|
+
// the provider's `selectDispatch` hook.
|
|
1456
|
+
const decision = dispatchDecision ?? defaultDispatchDecision(call.callForm);
|
|
1457
|
+
const primary = decision.primary;
|
|
1458
|
+
if (primary === 'free') {
|
|
1367
1459
|
return resolveFreeCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes);
|
|
1368
1460
|
}
|
|
1369
|
-
if (
|
|
1461
|
+
if (primary === 'constructor') {
|
|
1370
1462
|
return (resolveStaticCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes) ?? singleCandidate(tiered, call.argCount, 'constructor'));
|
|
1371
1463
|
}
|
|
1464
|
+
// primary === 'owner-scoped'
|
|
1372
1465
|
if (call.receiverTypeName) {
|
|
1373
1466
|
// Skip the owner-scoped MRO path when the tiered pool has genuine
|
|
1374
1467
|
// overload ambiguity that needs D1-D4+E handling, not D0.
|
|
1375
1468
|
const skipMember = (!!overloadHints || !!preComputedArgTypes) &&
|
|
1376
1469
|
countCallableCandidates(tiered.candidates, call.argCount, call.callForm) > 1;
|
|
1377
1470
|
// Try owner-scoped (resolveMemberCall) then file-scoped (resolveMemberCallByFile).
|
|
1471
|
+
// DAG: dispatchDecision.ancestryView selects instance vs singleton ancestry
|
|
1472
|
+
// for kind-aware MRO strategies. Ruby `Account.log` flows via 'singleton'.
|
|
1473
|
+
//
|
|
1474
|
+
// Singleton-ancestry miss MUST NOT degrade to the file-scoped fallback:
|
|
1475
|
+
// resolveMemberCallByFile matches by ownerId and would happily pick an
|
|
1476
|
+
// instance method defined on the same class, leaking instance dispatch
|
|
1477
|
+
// onto what was declared a class-method call. For singleton dispatch,
|
|
1478
|
+
// a miss either null-routes or falls through to `decision.fallback`.
|
|
1479
|
+
const singletonDispatch = decision.ancestryView === 'singleton';
|
|
1378
1480
|
const memberResult = (!skipMember
|
|
1379
|
-
? resolveMemberCall(call.receiverTypeName, call.calledName, currentFile, ctx, heritageMap, call.argCount)
|
|
1481
|
+
? resolveMemberCall(call.receiverTypeName, call.calledName, currentFile, ctx, heritageMap, call.argCount, decision.ancestryView)
|
|
1380
1482
|
: null) ??
|
|
1381
|
-
|
|
1483
|
+
(singletonDispatch
|
|
1484
|
+
? null
|
|
1485
|
+
: resolveMemberCallByFile(call.calledName, call.receiverTypeName, currentFile, ctx, call.argCount, call.callForm, overloadHints, preComputedArgTypes));
|
|
1382
1486
|
if (memberResult)
|
|
1383
1487
|
return memberResult;
|
|
1384
1488
|
// Module-alias narrowing runs as a FALLBACK, after owner/file-scoped
|
|
@@ -1411,7 +1515,19 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache, pr
|
|
|
1411
1515
|
// hierarchy. When the type is NOT in the index (PHP `mixed`, dynamic
|
|
1412
1516
|
// types, unresolvable aliases), the scoped resolvers had nothing to
|
|
1413
1517
|
// work with and singleCandidate is the correct last resort.
|
|
1518
|
+
//
|
|
1519
|
+
// DAG fallback override: when `select-dispatch` returned
|
|
1520
|
+
// `fallback: 'free-arity-narrowed'` (today: Ruby implicit-self bare
|
|
1521
|
+
// calls whose enclosing class doesn't define the method), fall through
|
|
1522
|
+
// to free-call resolution instead of null-routing. This preserves
|
|
1523
|
+
// existing free-call arity-narrowing heuristics for bare calls that
|
|
1524
|
+
// happen to target methods on unrelated classes.
|
|
1414
1525
|
if (typeResolves && typeResolves.candidates.length > 0) {
|
|
1526
|
+
if (decision.fallback === 'free-arity-narrowed') {
|
|
1527
|
+
const free = resolveFreeCall(call.calledName, currentFile, ctx, call.argCount, tiered, overloadHints, preComputedArgTypes);
|
|
1528
|
+
if (free)
|
|
1529
|
+
return free;
|
|
1530
|
+
}
|
|
1415
1531
|
return null; // null-route: type resolved, no candidate matched
|
|
1416
1532
|
}
|
|
1417
1533
|
return singleCandidate(tiered, call.argCount, call.callForm);
|
|
@@ -1567,7 +1683,14 @@ const resolveFieldOwnership = (receiverName, fieldName, filePath, ctx) => {
|
|
|
1567
1683
|
* Threaded out here so callers don't need a second `ctx.resolve(ownerType, ...)` call —
|
|
1568
1684
|
* this decouples callers from `ctx.resolve`'s per-file caching contract.
|
|
1569
1685
|
*/
|
|
1570
|
-
const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, heritageMap, argCount
|
|
1686
|
+
const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, heritageMap, argCount,
|
|
1687
|
+
/**
|
|
1688
|
+
* DAG-sourced ancestry selector. `'singleton'` routes through
|
|
1689
|
+
* `heritageMap.getSingletonAncestry(owner)` for class-method dispatch
|
|
1690
|
+
* (Ruby `Account.log` via `extend LoggerMixin`). Default / undefined
|
|
1691
|
+
* uses the walker's instance-dispatch behavior.
|
|
1692
|
+
*/
|
|
1693
|
+
ancestryView) => {
|
|
1571
1694
|
const typeResolved = ctx.resolve(receiverTypeName, filePath);
|
|
1572
1695
|
if (!typeResolved)
|
|
1573
1696
|
return undefined;
|
|
@@ -1595,8 +1718,15 @@ const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, herit
|
|
|
1595
1718
|
for (const candidate of typeResolved.candidates) {
|
|
1596
1719
|
if (!CLASS_LIKE_TYPES.has(candidate.type))
|
|
1597
1720
|
continue;
|
|
1721
|
+
// Singleton dispatch: when the DAG decision requested the singleton
|
|
1722
|
+
// ancestry view, pass `heritageMap.getSingletonAncestry` as the walker's
|
|
1723
|
+
// ancestry override. Kind-aware strategies (e.g. MroStrategy 'ruby-mixin')
|
|
1724
|
+
// honor the override by scanning it linearly in place of their default walk.
|
|
1725
|
+
const singletonOverride = ancestryView === 'singleton' && canWalkMRO && heritageMap
|
|
1726
|
+
? heritageMap.getSingletonAncestry(candidate.nodeId).map((e) => e.parentId)
|
|
1727
|
+
: undefined;
|
|
1598
1728
|
const def = canWalkMRO
|
|
1599
|
-
? lookupMethodByOwnerWithMRO(candidate.nodeId, methodName, heritageMap, ctx.model, mroStrategy, argCount)
|
|
1729
|
+
? lookupMethodByOwnerWithMRO(candidate.nodeId, methodName, heritageMap, ctx.model, mroStrategy, argCount, singletonOverride)
|
|
1600
1730
|
: ctx.model.methods.lookupMethodByOwner(candidate.nodeId, methodName, argCount);
|
|
1601
1731
|
if (!def)
|
|
1602
1732
|
continue;
|
|
@@ -1643,8 +1773,8 @@ const resolveMethodByOwner = (receiverTypeName, methodName, filePath, ctx, herit
|
|
|
1643
1773
|
* @param ctx - Resolution context
|
|
1644
1774
|
* @param heritageMap - Optional heritage map for MRO-aware ancestor walking
|
|
1645
1775
|
*/
|
|
1646
|
-
export const resolveMemberCall = (ownerType, methodName, currentFile, ctx, heritageMap, argCount) => {
|
|
1647
|
-
const resolved = resolveMethodByOwner(ownerType, methodName, currentFile, ctx, heritageMap, argCount);
|
|
1776
|
+
export const resolveMemberCall = (ownerType, methodName, currentFile, ctx, heritageMap, argCount, ancestryView) => {
|
|
1777
|
+
const resolved = resolveMethodByOwner(ownerType, methodName, currentFile, ctx, heritageMap, argCount, ancestryView);
|
|
1648
1778
|
if (!resolved)
|
|
1649
1779
|
return null;
|
|
1650
1780
|
return toResolveResult(resolved.def, resolved.tier);
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Ruby call routing logic.
|
|
3
3
|
*
|
|
4
|
-
* Ruby expresses imports
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Ruby expresses imports and property definitions as method calls rather
|
|
5
|
+
* than syntax-level constructs. This module provides a routing function
|
|
6
|
+
* used by the CLI call-processor, CLI parse-worker, and the web
|
|
7
|
+
* call-processor so that the classification logic lives in one place.
|
|
8
|
+
*
|
|
9
|
+
* Heritage (mixins: include/extend/prepend) was previously routed here
|
|
10
|
+
* but is now handled by heritageExtractor.extractFromCall before the
|
|
11
|
+
* call router runs. The router still returns 'skip' for these calls.
|
|
8
12
|
*
|
|
9
13
|
* NOTE: This file is intentionally duplicated in gitnexus-web/ because the
|
|
10
14
|
* two packages have separate build targets (Node native vs WASM/browser).
|
|
@@ -24,9 +28,6 @@ export type RubyCallRouting = {
|
|
|
24
28
|
kind: 'import';
|
|
25
29
|
importPath: string;
|
|
26
30
|
isRelative: boolean;
|
|
27
|
-
} | {
|
|
28
|
-
kind: 'heritage';
|
|
29
|
-
items: RubyHeritageItem[];
|
|
30
31
|
} | {
|
|
31
32
|
kind: 'properties';
|
|
32
33
|
items: RubyPropertyItem[];
|
|
@@ -35,11 +36,6 @@ export type RubyCallRouting = {
|
|
|
35
36
|
} | {
|
|
36
37
|
kind: 'skip';
|
|
37
38
|
};
|
|
38
|
-
export interface RubyHeritageItem {
|
|
39
|
-
enclosingClass: string;
|
|
40
|
-
mixinName: string;
|
|
41
|
-
heritageKind: 'include' | 'extend' | 'prepend';
|
|
42
|
-
}
|
|
43
39
|
export type RubyAccessorType = 'attr_accessor' | 'attr_reader' | 'attr_writer';
|
|
44
40
|
export interface RubyPropertyItem {
|
|
45
41
|
propName: string;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Ruby call routing logic.
|
|
3
3
|
*
|
|
4
|
-
* Ruby expresses imports
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Ruby expresses imports and property definitions as method calls rather
|
|
5
|
+
* than syntax-level constructs. This module provides a routing function
|
|
6
|
+
* used by the CLI call-processor, CLI parse-worker, and the web
|
|
7
|
+
* call-processor so that the classification logic lives in one place.
|
|
8
|
+
*
|
|
9
|
+
* Heritage (mixins: include/extend/prepend) was previously routed here
|
|
10
|
+
* but is now handled by heritageExtractor.extractFromCall before the
|
|
11
|
+
* call router runs. The router still returns 'skip' for these calls.
|
|
8
12
|
*
|
|
9
13
|
* NOTE: This file is intentionally duplicated in gitnexus-web/ because the
|
|
10
14
|
* two packages have separate build targets (Node native vs WASM/browser).
|
|
@@ -13,8 +17,6 @@
|
|
|
13
17
|
// ── Pre-allocated singletons for common return values ────────────────────────
|
|
14
18
|
const CALL_RESULT = { kind: 'call' };
|
|
15
19
|
const SKIP_RESULT = { kind: 'skip' };
|
|
16
|
-
/** Max depth for parent-walking loops to prevent pathological AST traversals */
|
|
17
|
-
const MAX_PARENT_DEPTH = 50;
|
|
18
20
|
// ── Routing function ────────────────────────────────────────────────────────
|
|
19
21
|
/**
|
|
20
22
|
* Classify a Ruby call node and extract its semantic payload.
|
|
@@ -42,35 +44,12 @@ export function routeRubyCall(calledName, callNode) {
|
|
|
42
44
|
}
|
|
43
45
|
return { kind: 'import', importPath, isRelative };
|
|
44
46
|
}
|
|
45
|
-
// ── include / extend / prepend
|
|
47
|
+
// ── include / extend / prepend — heritage (now handled by heritageExtractor) ─
|
|
48
|
+
// Call-based heritage is intercepted by heritageExtractor.extractFromCall
|
|
49
|
+
// before the call router runs. Return SKIP_RESULT so these calls don't
|
|
50
|
+
// fall through to normal call processing.
|
|
46
51
|
if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
|
|
47
|
-
|
|
48
|
-
let current = callNode.parent;
|
|
49
|
-
let depth = 0;
|
|
50
|
-
while (current && ++depth <= MAX_PARENT_DEPTH) {
|
|
51
|
-
if (current.type === 'class' || current.type === 'module') {
|
|
52
|
-
const nameNode = current.childForFieldName?.('name');
|
|
53
|
-
if (nameNode) {
|
|
54
|
-
enclosingClass = nameNode.text;
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
current = current.parent;
|
|
59
|
-
}
|
|
60
|
-
if (!enclosingClass)
|
|
61
|
-
return SKIP_RESULT;
|
|
62
|
-
const items = [];
|
|
63
|
-
const argList = callNode.childForFieldName?.('arguments');
|
|
64
|
-
for (const arg of argList?.children ?? []) {
|
|
65
|
-
if (arg.type === 'constant' || arg.type === 'scope_resolution') {
|
|
66
|
-
items.push({
|
|
67
|
-
enclosingClass,
|
|
68
|
-
mixinName: arg.text,
|
|
69
|
-
heritageKind: calledName,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return items.length > 0 ? { kind: 'heritage', items } : SKIP_RESULT;
|
|
52
|
+
return SKIP_RESULT;
|
|
74
53
|
}
|
|
75
54
|
// ── attr_accessor / attr_reader / attr_writer → property definitions ───
|
|
76
55
|
if (calledName === 'attr_accessor' ||
|
|
@@ -58,3 +58,78 @@ export interface CallExtractionConfig {
|
|
|
58
58
|
*/
|
|
59
59
|
typeAsReceiverHeuristic?: boolean;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* DAG stage 3 output: call record with receiver type and source discriminant.
|
|
63
|
+
*
|
|
64
|
+
* `receiverTypeName` is resolved via TypeEnv → constructor-map → class-as-receiver →
|
|
65
|
+
* mixed-chain, or synthesized by `inferImplicitReceiver`. `receiverSource` tags
|
|
66
|
+
* which path won and drives MRO strategy selection in stage 4.
|
|
67
|
+
*
|
|
68
|
+
* Invariants:
|
|
69
|
+
* - `receiverSource` MUST match how `receiverTypeName` was resolved; every
|
|
70
|
+
* discriminant must have a live reader and writer.
|
|
71
|
+
* - `hint` is opaque to shared stages; only the same provider's `selectDispatch` reads it.
|
|
72
|
+
*
|
|
73
|
+
* @see language-provider.ts § inferImplicitReceiver, selectDispatch
|
|
74
|
+
*/
|
|
75
|
+
export interface ReceiverEnriched {
|
|
76
|
+
readonly calledName: string;
|
|
77
|
+
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
|
78
|
+
readonly receiverName: string | undefined;
|
|
79
|
+
readonly receiverTypeName: string | undefined;
|
|
80
|
+
readonly receiverSource: 'none' | 'typed-binding' | 'constructor-map' | 'class-as-receiver' | 'mixed-chain' | 'implicit-self';
|
|
81
|
+
/** Free-form hint from the provider hook; opaque to shared stages. */
|
|
82
|
+
readonly hint?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Provider hook output for `LanguageProvider.inferImplicitReceiver` (DAG stage 3).
|
|
86
|
+
*
|
|
87
|
+
* Overlay applied to `ReceiverEnriched` when an implicit receiver is synthesized.
|
|
88
|
+
* Ruby example: bare `serialize` inside `Account#call_serialize` →
|
|
89
|
+
* `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
|
|
90
|
+
* receiverSource: 'implicit-self', hint: 'instance' }`
|
|
91
|
+
*
|
|
92
|
+
* Invariants:
|
|
93
|
+
* - `receiverSource` is always `'implicit-self'` — the only variant this type produces.
|
|
94
|
+
* - `callForm` is always `'member'` — the rewrite converts bare-call to method invocation.
|
|
95
|
+
* - `hint` is opaque to shared stages; consumed by the same language's `selectDispatch`.
|
|
96
|
+
*/
|
|
97
|
+
export interface ImplicitReceiverOverride {
|
|
98
|
+
readonly callForm: 'free' | 'member' | 'constructor';
|
|
99
|
+
readonly receiverName: string;
|
|
100
|
+
readonly receiverTypeName: string;
|
|
101
|
+
readonly receiverSource: Extract<ReceiverEnriched['receiverSource'], 'implicit-self'>;
|
|
102
|
+
/** Free-form language tag (e.g. Ruby sets 'singleton' for `def self.foo`
|
|
103
|
+
* method bodies). Consumed by the same language's `selectDispatch` hook. */
|
|
104
|
+
readonly hint?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* DAG stage 4 output: dispatch strategy for resolving the target method.
|
|
108
|
+
*
|
|
109
|
+
* Encodes which resolver branch to try first and an optional fallback.
|
|
110
|
+
* Stage 5 delegates to `resolveMemberCall`, `resolveFreeCall`, or
|
|
111
|
+
* `resolveStaticCall` based on `primary`.
|
|
112
|
+
*
|
|
113
|
+
* - `primary`: `'owner-scoped'` = MRO walk, `'free'` = arity-tiered global lookup,
|
|
114
|
+
* `'constructor'` = type instantiation.
|
|
115
|
+
* - `fallback`: Only `'free-arity-narrowed'` exists; used by Ruby implicit-self
|
|
116
|
+
* to degrade to arity-tiered free lookup when the MRO walk misses.
|
|
117
|
+
* - `ancestryView`: Ruby `'ruby-mixin'` only. `'singleton'` walks extend providers
|
|
118
|
+
* only; a miss NEVER falls through to file-scoped lookup (enforced in
|
|
119
|
+
* resolveCallTarget). `'instance'` is the default.
|
|
120
|
+
*
|
|
121
|
+
* Common patterns:
|
|
122
|
+
* - `{primary: 'constructor'}` — constructor call
|
|
123
|
+
* - `{primary: 'owner-scoped'}` — member call with known type
|
|
124
|
+
* - `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}` — Ruby implicit-self
|
|
125
|
+
* - `{primary: 'owner-scoped', ancestryView: 'singleton'}` — Ruby class-method call
|
|
126
|
+
*
|
|
127
|
+
* @see language-provider.ts § selectDispatch
|
|
128
|
+
* @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
|
|
129
|
+
*/
|
|
130
|
+
export interface DispatchDecision {
|
|
131
|
+
readonly primary: 'owner-scoped' | 'free' | 'constructor';
|
|
132
|
+
readonly fallback?: 'free-arity-narrowed';
|
|
133
|
+
readonly ancestryView?: 'instance' | 'singleton';
|
|
134
|
+
readonly hint?: string;
|
|
135
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HeritageExtractionConfig } from '../../heritage-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Go heritage extraction config.
|
|
4
|
+
*
|
|
5
|
+
* Go struct embedding: the tree-sitter query matches ALL field_declarations
|
|
6
|
+
* with type_identifier, but only anonymous fields (no name) are embedded.
|
|
7
|
+
* Named fields like `Breed string` also match — skip them.
|
|
8
|
+
*
|
|
9
|
+
* The shouldSkipExtends hook checks if the extends node's parent is a
|
|
10
|
+
* field_declaration with a named field child, indicating a regular
|
|
11
|
+
* (non-embedded) field that should not produce a heritage record.
|
|
12
|
+
*/
|
|
13
|
+
export declare const goHeritageConfig: HeritageExtractionConfig;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts
|
|
2
|
+
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Go heritage extraction config.
|
|
5
|
+
*
|
|
6
|
+
* Go struct embedding: the tree-sitter query matches ALL field_declarations
|
|
7
|
+
* with type_identifier, but only anonymous fields (no name) are embedded.
|
|
8
|
+
* Named fields like `Breed string` also match — skip them.
|
|
9
|
+
*
|
|
10
|
+
* The shouldSkipExtends hook checks if the extends node's parent is a
|
|
11
|
+
* field_declaration with a named field child, indicating a regular
|
|
12
|
+
* (non-embedded) field that should not produce a heritage record.
|
|
13
|
+
*/
|
|
14
|
+
export const goHeritageConfig = {
|
|
15
|
+
language: SupportedLanguages.Go,
|
|
16
|
+
shouldSkipExtends(extendsNode) {
|
|
17
|
+
const fieldDecl = extendsNode.parent;
|
|
18
|
+
return fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName?.('name') != null;
|
|
19
|
+
},
|
|
20
|
+
};
|