gitnexus 1.6.2-rc.21 → 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/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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HeritageExtractionConfig } from '../../heritage-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Ruby heritage extraction config.
|
|
4
|
+
*
|
|
5
|
+
* Ruby expresses inheritance in two ways, and only one of them has
|
|
6
|
+
* dedicated tree-sitter heritage captures:
|
|
7
|
+
*
|
|
8
|
+
* 1. Class inheritance (`class A < B`) produces standard
|
|
9
|
+
* `@heritage.extends` captures and flows through the generic
|
|
10
|
+
* capture-based `extract` hook (not defined here — the factory
|
|
11
|
+
* handles it).
|
|
12
|
+
* 2. Mixin calls (`include`/`extend`/`prepend`) have no dedicated
|
|
13
|
+
* heritage captures; they surface as ordinary call sites. The
|
|
14
|
+
* `callBasedHeritage` hook below intercepts them before the call
|
|
15
|
+
* router, absorbing the mixin routing logic that previously lived
|
|
16
|
+
* in call-routing.ts (routeRubyCall).
|
|
17
|
+
*/
|
|
18
|
+
export declare const rubyHeritageConfig: HeritageExtractionConfig;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts
|
|
2
|
+
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Maximum parent depth for enclosing class/module walk.
|
|
5
|
+
* Prevents runaway walks on malformed/deeply-nested ASTs.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_PARENT_DEPTH = 50;
|
|
8
|
+
/**
|
|
9
|
+
* Walk up the AST from a call node to find the enclosing class or module name.
|
|
10
|
+
* Ruby include/extend/prepend calls must be inside a class or module body.
|
|
11
|
+
*/
|
|
12
|
+
function findEnclosingClassName(callNode) {
|
|
13
|
+
let current = callNode.parent;
|
|
14
|
+
let depth = 0;
|
|
15
|
+
while (current && ++depth <= MAX_PARENT_DEPTH) {
|
|
16
|
+
if (current.type === 'class' || current.type === 'module') {
|
|
17
|
+
const nameNode = current.childForFieldName?.('name');
|
|
18
|
+
if (nameNode)
|
|
19
|
+
return nameNode.text;
|
|
20
|
+
}
|
|
21
|
+
current = current.parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/** Ruby heritage call names that express mixin inclusion. */
|
|
26
|
+
const RUBY_HERITAGE_CALL_NAMES = new Set(['include', 'extend', 'prepend']);
|
|
27
|
+
/**
|
|
28
|
+
* Ruby heritage extraction config.
|
|
29
|
+
*
|
|
30
|
+
* Ruby expresses inheritance in two ways, and only one of them has
|
|
31
|
+
* dedicated tree-sitter heritage captures:
|
|
32
|
+
*
|
|
33
|
+
* 1. Class inheritance (`class A < B`) produces standard
|
|
34
|
+
* `@heritage.extends` captures and flows through the generic
|
|
35
|
+
* capture-based `extract` hook (not defined here — the factory
|
|
36
|
+
* handles it).
|
|
37
|
+
* 2. Mixin calls (`include`/`extend`/`prepend`) have no dedicated
|
|
38
|
+
* heritage captures; they surface as ordinary call sites. The
|
|
39
|
+
* `callBasedHeritage` hook below intercepts them before the call
|
|
40
|
+
* router, absorbing the mixin routing logic that previously lived
|
|
41
|
+
* in call-routing.ts (routeRubyCall).
|
|
42
|
+
*/
|
|
43
|
+
export const rubyHeritageConfig = {
|
|
44
|
+
language: SupportedLanguages.Ruby,
|
|
45
|
+
callBasedHeritage: {
|
|
46
|
+
callNames: RUBY_HERITAGE_CALL_NAMES,
|
|
47
|
+
extract(calledName, callNode, _filePath) {
|
|
48
|
+
const enclosingClass = findEnclosingClassName(callNode);
|
|
49
|
+
if (!enclosingClass)
|
|
50
|
+
return [];
|
|
51
|
+
const results = [];
|
|
52
|
+
const argList = callNode.childForFieldName?.('arguments');
|
|
53
|
+
for (const arg of argList?.children ?? []) {
|
|
54
|
+
if (arg.type === 'constant' || arg.type === 'scope_resolution') {
|
|
55
|
+
results.push({
|
|
56
|
+
className: enclosingClass,
|
|
57
|
+
parentName: arg.text,
|
|
58
|
+
kind: calledName, // 'include' | 'extend' | 'prepend'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic table-driven heritage extractor factory.
|
|
3
|
+
*
|
|
4
|
+
* Follows the same config+factory pattern as method-extractors/generic.ts,
|
|
5
|
+
* field-extractors/generic.ts, call-extractors/generic.ts, and
|
|
6
|
+
* variable-extractors/generic.ts.
|
|
7
|
+
*
|
|
8
|
+
* Languages with custom extraction hooks (Go: shouldSkipExtends, Ruby:
|
|
9
|
+
* callBasedHeritage) pass a full HeritageExtractionConfig. Languages
|
|
10
|
+
* that use the default capture-based extraction can pass just the
|
|
11
|
+
* SupportedLanguages enum value — no per-language config file needed.
|
|
12
|
+
*/
|
|
13
|
+
import type { SupportedLanguages } from '../../../_shared/index.js';
|
|
14
|
+
import type { HeritageExtractionConfig, HeritageExtractor } from '../heritage-types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Create a HeritageExtractor from a declarative config or a language enum.
|
|
17
|
+
*
|
|
18
|
+
* When a full HeritageExtractionConfig is provided, custom hooks
|
|
19
|
+
* (shouldSkipExtends, callBasedHeritage) drive the extraction.
|
|
20
|
+
* When only a SupportedLanguages value is provided, the factory produces
|
|
21
|
+
* a default extractor that handles the standard @heritage.* captures.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createHeritageExtractor(config: HeritageExtractionConfig | SupportedLanguages): HeritageExtractor;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// gitnexus/src/core/ingestion/heritage-extractors/generic.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create a HeritageExtractor from a declarative config or a language enum.
|
|
4
|
+
*
|
|
5
|
+
* When a full HeritageExtractionConfig is provided, custom hooks
|
|
6
|
+
* (shouldSkipExtends, callBasedHeritage) drive the extraction.
|
|
7
|
+
* When only a SupportedLanguages value is provided, the factory produces
|
|
8
|
+
* a default extractor that handles the standard @heritage.* captures.
|
|
9
|
+
*/
|
|
10
|
+
export function createHeritageExtractor(config) {
|
|
11
|
+
const actualConfig = typeof config === 'string' ? { language: config } : config;
|
|
12
|
+
const callNameSet = actualConfig.callBasedHeritage?.callNames;
|
|
13
|
+
return {
|
|
14
|
+
language: actualConfig.language,
|
|
15
|
+
extract(captureMap, context) {
|
|
16
|
+
const classNode = captureMap['heritage.class'];
|
|
17
|
+
if (!classNode)
|
|
18
|
+
return [];
|
|
19
|
+
const className = classNode.text;
|
|
20
|
+
const results = [];
|
|
21
|
+
const extendsNode = captureMap['heritage.extends'];
|
|
22
|
+
if (extendsNode) {
|
|
23
|
+
if (!actualConfig.shouldSkipExtends?.(extendsNode)) {
|
|
24
|
+
results.push({ className, parentName: extendsNode.text, kind: 'extends' });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const implementsNode = captureMap['heritage.implements'];
|
|
28
|
+
if (implementsNode) {
|
|
29
|
+
results.push({ className, parentName: implementsNode.text, kind: 'implements' });
|
|
30
|
+
}
|
|
31
|
+
const traitNode = captureMap['heritage.trait'];
|
|
32
|
+
if (traitNode) {
|
|
33
|
+
results.push({ className, parentName: traitNode.text, kind: 'trait-impl' });
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
},
|
|
37
|
+
...(callNameSet
|
|
38
|
+
? {
|
|
39
|
+
extractFromCall(calledName, callNode, context) {
|
|
40
|
+
if (!callNameSet.has(calledName))
|
|
41
|
+
return null;
|
|
42
|
+
return actualConfig.callBasedHeritage.extract(calledName, callNode, context.filePath);
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
: {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -38,6 +38,15 @@ export declare const processHeritageFromExtracted: (graph: KnowledgeGraph, extra
|
|
|
38
38
|
* {@link ExtractedHeritage} rows without mutating the graph. Used on the
|
|
39
39
|
* sequential pipeline path so `buildHeritageMap(..., ctx)` can run before
|
|
40
40
|
* `processCalls` (worker path defers calls until heritage from all chunks exists).
|
|
41
|
+
*
|
|
42
|
+
* This prepass extracts BOTH capture-based heritage (`@heritage.*` — extends /
|
|
43
|
+
* implements / trait-impl) AND call-based heritage (`@call.name` routed through
|
|
44
|
+
* `heritageExtractor.extractFromCall` — Ruby `include` / `extend` / `prepend`).
|
|
45
|
+
* Without the second pass, sequential-mode `sequentialHeritageMap` would not
|
|
46
|
+
* know about Ruby mixin ancestry before `processCalls` resolves calls against
|
|
47
|
+
* it, silently dropping mixed-in methods from the graph. This function stays
|
|
48
|
+
* read-only — `processCalls` still owns emission of heritage graph edges via
|
|
49
|
+
* its `rubyHeritage` return path.
|
|
41
50
|
*/
|
|
42
51
|
export declare function extractExtractedHeritageFromFiles(files: {
|
|
43
52
|
path: string;
|
|
@@ -54,6 +54,69 @@ const resolveHeritageId = (name, filePath, ctx, fallbackLabel, fallbackKey) => {
|
|
|
54
54
|
confidence: TIER_CONFIDENCE['global'],
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a single HeritageInfo to a graph edge, using the same resolution
|
|
59
|
+
* logic as processHeritageFromExtracted. This bridges the heritage extractor
|
|
60
|
+
* output format to the graph-resolution side.
|
|
61
|
+
*/
|
|
62
|
+
const resolveAndAddHeritageEdge = (graph, item, filePath, language, ctx) => {
|
|
63
|
+
if (item.kind === 'extends') {
|
|
64
|
+
const { type: relType, idPrefix } = resolveExtendsType(item.parentName, filePath, ctx, getHeritageStrategyForLanguage(language));
|
|
65
|
+
const child = resolveHeritageId(item.className, filePath, ctx, 'Class', `${filePath}:${item.className}`);
|
|
66
|
+
const parent = resolveHeritageId(item.parentName, filePath, ctx, idPrefix);
|
|
67
|
+
if (child.id && parent.id && child.id !== parent.id) {
|
|
68
|
+
graph.addRelationship({
|
|
69
|
+
id: generateId(relType, `${child.id}->${parent.id}`),
|
|
70
|
+
sourceId: child.id,
|
|
71
|
+
targetId: parent.id,
|
|
72
|
+
type: relType,
|
|
73
|
+
confidence: Math.sqrt(child.confidence * parent.confidence),
|
|
74
|
+
reason: '',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (item.kind === 'implements') {
|
|
79
|
+
const cls = resolveHeritageId(item.className, filePath, ctx, 'Class', `${filePath}:${item.className}`);
|
|
80
|
+
const iface = resolveHeritageId(item.parentName, filePath, ctx, 'Interface');
|
|
81
|
+
if (cls.id && iface.id) {
|
|
82
|
+
graph.addRelationship({
|
|
83
|
+
id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
|
|
84
|
+
sourceId: cls.id,
|
|
85
|
+
targetId: iface.id,
|
|
86
|
+
type: 'IMPLEMENTS',
|
|
87
|
+
confidence: Math.sqrt(cls.confidence * iface.confidence),
|
|
88
|
+
reason: '',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (item.kind === 'trait-impl' ||
|
|
93
|
+
item.kind === 'include' ||
|
|
94
|
+
item.kind === 'extend' ||
|
|
95
|
+
item.kind === 'prepend') {
|
|
96
|
+
// Fallback label for an unresolved child name. Rust `trait-impl` children
|
|
97
|
+
// are structs; Ruby mixin children are classes or modules (Trait). For
|
|
98
|
+
// Ruby mixin kinds the common case resolves through the type registry
|
|
99
|
+
// post-plan-001, so the fallback only fires for true-unresolved references
|
|
100
|
+
// (e.g. mixin inside a singleton_class). `Class` is strictly better than
|
|
101
|
+
// `Struct` there because it matches the label the structure phase would
|
|
102
|
+
// emit for a Ruby `class` — the dominant shape. Ruby modules that fail
|
|
103
|
+
// to resolve still lose their `Trait` label in the synthesized id, but
|
|
104
|
+
// they fail to resolve rarely and the tradeoff is documented.
|
|
105
|
+
const childFallbackLabel = item.kind === 'trait-impl' ? 'Struct' : 'Class';
|
|
106
|
+
const strct = resolveHeritageId(item.className, filePath, ctx, childFallbackLabel, `${filePath}:${item.className}`);
|
|
107
|
+
const trait = resolveHeritageId(item.parentName, filePath, ctx, 'Trait');
|
|
108
|
+
if (strct.id && trait.id) {
|
|
109
|
+
graph.addRelationship({
|
|
110
|
+
id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}:${item.kind}`),
|
|
111
|
+
sourceId: strct.id,
|
|
112
|
+
targetId: trait.id,
|
|
113
|
+
type: 'IMPLEMENTS',
|
|
114
|
+
confidence: Math.sqrt(strct.confidence * trait.confidence),
|
|
115
|
+
reason: item.kind,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
57
120
|
export const processHeritage = async (graph, files, astCache, ctx, onProgress) => {
|
|
58
121
|
const parser = await loadParser();
|
|
59
122
|
const logSkipped = isVerboseIngestionEnabled();
|
|
@@ -98,78 +161,31 @@ export const processHeritage = async (graph, files, astCache, ctx, onProgress) =
|
|
|
98
161
|
let query;
|
|
99
162
|
let matches;
|
|
100
163
|
try {
|
|
101
|
-
const
|
|
102
|
-
query = new Parser.Query(
|
|
164
|
+
const treeSitterLang = parser.getLanguage();
|
|
165
|
+
query = new Parser.Query(treeSitterLang, queryStr);
|
|
103
166
|
matches = query.matches(tree.rootNode);
|
|
104
167
|
}
|
|
105
168
|
catch (queryError) {
|
|
106
169
|
console.warn(`Heritage query error for ${file.path}:`, queryError);
|
|
107
170
|
continue;
|
|
108
171
|
}
|
|
109
|
-
// 4. Process heritage matches
|
|
172
|
+
// 4. Process heritage matches via provider heritage extractor
|
|
173
|
+
const heritageExtractor = provider.heritageExtractor;
|
|
110
174
|
matches.forEach((match) => {
|
|
111
175
|
const captureMap = {};
|
|
112
176
|
match.captures.forEach((c) => {
|
|
113
177
|
captureMap[c.name] = c.node;
|
|
114
178
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const parentClassName = captureMap['heritage.extends'].text;
|
|
126
|
-
const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, ctx, getHeritageStrategyForLanguage(language));
|
|
127
|
-
const child = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
|
|
128
|
-
const parent = resolveHeritageId(parentClassName, file.path, ctx, idPrefix);
|
|
129
|
-
if (child.id && parent.id && child.id !== parent.id) {
|
|
130
|
-
graph.addRelationship({
|
|
131
|
-
id: generateId(relType, `${child.id}->${parent.id}`),
|
|
132
|
-
sourceId: child.id,
|
|
133
|
-
targetId: parent.id,
|
|
134
|
-
type: relType,
|
|
135
|
-
confidence: Math.sqrt(child.confidence * parent.confidence),
|
|
136
|
-
reason: '',
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// IMPLEMENTS: Class implements Interface (TypeScript only)
|
|
141
|
-
if (captureMap['heritage.class'] && captureMap['heritage.implements']) {
|
|
142
|
-
const className = captureMap['heritage.class'].text;
|
|
143
|
-
const interfaceName = captureMap['heritage.implements'].text;
|
|
144
|
-
const cls = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
|
|
145
|
-
const iface = resolveHeritageId(interfaceName, file.path, ctx, 'Interface');
|
|
146
|
-
if (cls.id && iface.id) {
|
|
147
|
-
graph.addRelationship({
|
|
148
|
-
id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
|
|
149
|
-
sourceId: cls.id,
|
|
150
|
-
targetId: iface.id,
|
|
151
|
-
type: 'IMPLEMENTS',
|
|
152
|
-
confidence: Math.sqrt(cls.confidence * iface.confidence),
|
|
153
|
-
reason: '',
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// IMPLEMENTS (Rust): impl Trait for Struct
|
|
158
|
-
if (captureMap['heritage.trait'] && captureMap['heritage.class']) {
|
|
159
|
-
const structName = captureMap['heritage.class'].text;
|
|
160
|
-
const traitName = captureMap['heritage.trait'].text;
|
|
161
|
-
const strct = resolveHeritageId(structName, file.path, ctx, 'Struct', `${file.path}:${structName}`);
|
|
162
|
-
const trait = resolveHeritageId(traitName, file.path, ctx, 'Trait');
|
|
163
|
-
if (strct.id && trait.id) {
|
|
164
|
-
graph.addRelationship({
|
|
165
|
-
id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}`),
|
|
166
|
-
sourceId: strct.id,
|
|
167
|
-
targetId: trait.id,
|
|
168
|
-
type: 'IMPLEMENTS',
|
|
169
|
-
confidence: Math.sqrt(strct.confidence * trait.confidence),
|
|
170
|
-
reason: 'trait-impl',
|
|
171
|
-
});
|
|
172
|
-
}
|
|
179
|
+
if (!captureMap['heritage.class'])
|
|
180
|
+
return;
|
|
181
|
+
if (!heritageExtractor)
|
|
182
|
+
return;
|
|
183
|
+
const heritageItems = heritageExtractor.extract(captureMap, {
|
|
184
|
+
filePath: file.path,
|
|
185
|
+
language,
|
|
186
|
+
});
|
|
187
|
+
for (const item of heritageItems) {
|
|
188
|
+
resolveAndAddHeritageEdge(graph, item, file.path, language, ctx);
|
|
173
189
|
}
|
|
174
190
|
});
|
|
175
191
|
// Tree is now owned by the LRU cache — no manual delete needed
|
|
@@ -228,7 +244,11 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, ctx
|
|
|
228
244
|
h.kind === 'include' ||
|
|
229
245
|
h.kind === 'extend' ||
|
|
230
246
|
h.kind === 'prepend') {
|
|
231
|
-
|
|
247
|
+
// See the per-item call above (processHeritageFromExtractedItem) for
|
|
248
|
+
// rationale: `Class` is the correct fallback for Ruby mixin kinds,
|
|
249
|
+
// `Struct` stays the Rust `trait-impl` default.
|
|
250
|
+
const childFallbackLabel = h.kind === 'trait-impl' ? 'Struct' : 'Class';
|
|
251
|
+
const strct = resolveHeritageId(h.className, h.filePath, ctx, childFallbackLabel, `${h.filePath}:${h.className}`);
|
|
232
252
|
const trait = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
|
|
233
253
|
if (strct.id && trait.id) {
|
|
234
254
|
graph.addRelationship({
|
|
@@ -249,6 +269,15 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, ctx
|
|
|
249
269
|
* {@link ExtractedHeritage} rows without mutating the graph. Used on the
|
|
250
270
|
* sequential pipeline path so `buildHeritageMap(..., ctx)` can run before
|
|
251
271
|
* `processCalls` (worker path defers calls until heritage from all chunks exists).
|
|
272
|
+
*
|
|
273
|
+
* This prepass extracts BOTH capture-based heritage (`@heritage.*` — extends /
|
|
274
|
+
* implements / trait-impl) AND call-based heritage (`@call.name` routed through
|
|
275
|
+
* `heritageExtractor.extractFromCall` — Ruby `include` / `extend` / `prepend`).
|
|
276
|
+
* Without the second pass, sequential-mode `sequentialHeritageMap` would not
|
|
277
|
+
* know about Ruby mixin ancestry before `processCalls` resolves calls against
|
|
278
|
+
* it, silently dropping mixed-in methods from the graph. This function stays
|
|
279
|
+
* read-only — `processCalls` still owns emission of heritage graph edges via
|
|
280
|
+
* its `rubyHeritage` return path.
|
|
252
281
|
*/
|
|
253
282
|
export async function extractExtractedHeritageFromFiles(files, astCache) {
|
|
254
283
|
const parser = await loadParser();
|
|
@@ -283,40 +312,46 @@ export async function extractExtractedHeritageFromFiles(files, astCache) {
|
|
|
283
312
|
catch {
|
|
284
313
|
continue;
|
|
285
314
|
}
|
|
315
|
+
const callBasedEnabled = !!provider.heritageExtractor?.extractFromCall;
|
|
286
316
|
for (const match of matches) {
|
|
287
317
|
const captureMap = {};
|
|
288
318
|
match.captures.forEach((c) => {
|
|
289
319
|
captureMap[c.name] = c.node;
|
|
290
320
|
});
|
|
291
321
|
if (captureMap['heritage.class']) {
|
|
292
|
-
if (
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
322
|
+
if (provider.heritageExtractor) {
|
|
323
|
+
const heritageItems = provider.heritageExtractor.extract(captureMap, {
|
|
324
|
+
filePath: file.path,
|
|
325
|
+
language,
|
|
326
|
+
});
|
|
327
|
+
for (const item of heritageItems) {
|
|
297
328
|
out.push({
|
|
298
329
|
filePath: file.path,
|
|
299
|
-
className:
|
|
300
|
-
parentName:
|
|
301
|
-
kind:
|
|
330
|
+
className: item.className,
|
|
331
|
+
parentName: item.parentName,
|
|
332
|
+
kind: item.kind,
|
|
302
333
|
});
|
|
303
334
|
}
|
|
304
335
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
// Call-based heritage (e.g. Ruby include/extend/prepend). Matches the
|
|
339
|
+
// routing the worker path performs inline in parse-worker.ts — see the
|
|
340
|
+
// `provider.heritageExtractor?.extractFromCall` branch there. We only
|
|
341
|
+
// need call-based records here; other @call captures are consumed by
|
|
342
|
+
// processCalls later in the sequential loop.
|
|
343
|
+
if (callBasedEnabled && captureMap['call'] && captureMap['call.name']) {
|
|
344
|
+
const calledName = captureMap['call.name'].text;
|
|
345
|
+
const heritageItems = provider.heritageExtractor.extractFromCall(calledName, captureMap['call'], { filePath: file.path, language });
|
|
346
|
+
if (heritageItems) {
|
|
347
|
+
for (const item of heritageItems) {
|
|
348
|
+
out.push({
|
|
349
|
+
filePath: file.path,
|
|
350
|
+
className: item.className,
|
|
351
|
+
parentName: item.parentName,
|
|
352
|
+
kind: item.kind,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
320
355
|
}
|
|
321
356
|
}
|
|
322
357
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the language-agnostic heritage extraction pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Follows the same pattern as call-types.ts, variable-types.ts, and
|
|
5
|
+
* method-types.ts: defines the domain interfaces consumed by
|
|
6
|
+
* createHeritageExtractor() and the per-language configs.
|
|
7
|
+
*
|
|
8
|
+
* Heritage extraction handles extends/implements/trait-impl captures from
|
|
9
|
+
* tree-sitter queries, plus call-based heritage for languages like Ruby
|
|
10
|
+
* (include/extend/prepend expressed as method calls).
|
|
11
|
+
*/
|
|
12
|
+
import type { SupportedLanguages } from '../../_shared/index.js';
|
|
13
|
+
import type { SyntaxNode } from './utils/ast-helpers.js';
|
|
14
|
+
import type { CaptureMap } from './language-provider.js';
|
|
15
|
+
/**
|
|
16
|
+
* Per-match heritage extraction result. The parse worker adds filePath to
|
|
17
|
+
* produce the final {@link ExtractedHeritage} that enters the resolution
|
|
18
|
+
* pipeline (heritage-processor.ts / heritage-map.ts).
|
|
19
|
+
*/
|
|
20
|
+
export interface HeritageInfo {
|
|
21
|
+
className: string;
|
|
22
|
+
parentName: string;
|
|
23
|
+
/** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */
|
|
24
|
+
kind: string;
|
|
25
|
+
}
|
|
26
|
+
export interface HeritageExtractorContext {
|
|
27
|
+
filePath: string;
|
|
28
|
+
language: SupportedLanguages;
|
|
29
|
+
}
|
|
30
|
+
export interface HeritageExtractor {
|
|
31
|
+
readonly language: SupportedLanguages;
|
|
32
|
+
/**
|
|
33
|
+
* Extract heritage records from tree-sitter @heritage.* captures.
|
|
34
|
+
*
|
|
35
|
+
* @param captureMap The capture map from a single tree-sitter match
|
|
36
|
+
* @param context File path and language context
|
|
37
|
+
* @returns Array of heritage records (may be empty if captures don't match)
|
|
38
|
+
*/
|
|
39
|
+
extract(captureMap: CaptureMap, context: HeritageExtractorContext): HeritageInfo[];
|
|
40
|
+
/**
|
|
41
|
+
* Extract heritage from a call node (for languages where heritage is
|
|
42
|
+
* expressed as method calls, e.g., Ruby include/extend/prepend).
|
|
43
|
+
*
|
|
44
|
+
* @param calledName The method name (e.g. 'include', 'extend', 'prepend')
|
|
45
|
+
* @param callNode The tree-sitter call AST node
|
|
46
|
+
* @param context File path and language context
|
|
47
|
+
* @returns Heritage records if the call is heritage-related, or null to
|
|
48
|
+
* fall through to the call router / normal call handling.
|
|
49
|
+
*/
|
|
50
|
+
extractFromCall?(calledName: string, callNode: SyntaxNode, context: HeritageExtractorContext): HeritageInfo[] | null;
|
|
51
|
+
}
|
|
52
|
+
export interface HeritageExtractionConfig {
|
|
53
|
+
language: SupportedLanguages;
|
|
54
|
+
/**
|
|
55
|
+
* Called for heritage.extends captures. Return true to skip this extends
|
|
56
|
+
* capture. Used by Go to skip named struct fields that match the
|
|
57
|
+
* field_declaration pattern but are not anonymous embeddings.
|
|
58
|
+
*
|
|
59
|
+
* Default: never skip (all extends captures are valid).
|
|
60
|
+
*/
|
|
61
|
+
shouldSkipExtends?: (extendsNode: SyntaxNode) => boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Call-based heritage extraction for languages where heritage is expressed
|
|
64
|
+
* as method calls (e.g., Ruby include/extend/prepend).
|
|
65
|
+
*
|
|
66
|
+
* callNames: set of method names that trigger heritage extraction.
|
|
67
|
+
* extract: extract heritage items from the call node + method name.
|
|
68
|
+
*/
|
|
69
|
+
callBasedHeritage?: {
|
|
70
|
+
readonly callNames: ReadonlySet<string>;
|
|
71
|
+
extract(calledName: string, callNode: SyntaxNode, filePath: string): HeritageInfo[];
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import type { SupportedLanguages, MroStrategy } from '../../_shared/index.js';
|
|
12
12
|
import type { LanguageTypeConfig } from './type-extractors/types.js';
|
|
13
13
|
import type { CallRouter } from './call-routing.js';
|
|
14
|
-
import type { CallExtractor } from './call-types.js';
|
|
14
|
+
import type { CallExtractor, DispatchDecision, ImplicitReceiverOverride, ReceiverEnriched } from './call-types.js';
|
|
15
15
|
import type { ClassExtractor } from './class-types.js';
|
|
16
16
|
import type { ExportChecker } from './export-detection.js';
|
|
17
17
|
import type { FieldExtractor } from './field-extractor.js';
|
|
18
|
+
import type { HeritageExtractor } from './heritage-types.js';
|
|
18
19
|
import type { MethodExtractor } from './method-types.js';
|
|
19
20
|
import type { VariableExtractor } from './variable-types.js';
|
|
20
21
|
import type { ImportResolverFn } from './import-resolvers/types.js';
|
|
@@ -143,6 +144,13 @@ interface LanguageProviderConfig {
|
|
|
143
144
|
* Uses the same provider-driven strategy pattern as method/field extraction so
|
|
144
145
|
* namespace/package/module rules stay language-specific. */
|
|
145
146
|
readonly classExtractor?: ClassExtractor;
|
|
147
|
+
/** Heritage extractor for extracting extends/implements/trait-impl relationships
|
|
148
|
+
* from tree-sitter @heritage.* captures and call-based heritage (e.g., Ruby
|
|
149
|
+
* include/extend/prepend). Produced by createHeritageExtractor() — pass a
|
|
150
|
+
* SupportedLanguages value for default behaviour or a full
|
|
151
|
+
* HeritageExtractionConfig for languages with custom hooks (Go, Ruby).
|
|
152
|
+
* All tree-sitter providers MUST supply this. */
|
|
153
|
+
readonly heritageExtractor?: HeritageExtractor;
|
|
146
154
|
/** Extract a semantic description for a definition node (e.g., PHP Eloquent
|
|
147
155
|
* property arrays, relation method descriptions).
|
|
148
156
|
* Default: undefined (no description extraction). */
|
|
@@ -151,6 +159,66 @@ interface LanguageProviderConfig {
|
|
|
151
159
|
* When true, the worker extracts routes via the language's route extraction logic.
|
|
152
160
|
* Default: undefined (no route files). */
|
|
153
161
|
readonly isRouteFile?: (filePath: string) => boolean;
|
|
162
|
+
/**
|
|
163
|
+
* DAG stage 3 hook: synthesize an implicit receiver when the call site omits one.
|
|
164
|
+
*
|
|
165
|
+
* Runs after shared inference (TypeEnv → constructor-map → class-as-receiver →
|
|
166
|
+
* mixed-chain). Return an `ImplicitReceiverOverride` to overlay all fields onto
|
|
167
|
+
* `ReceiverEnriched`; return null to keep current state and proceed to stage 4.
|
|
168
|
+
*
|
|
169
|
+
* Constraints: MUST return null when an explicit receiver is already set, at
|
|
170
|
+
* top-level scope, or for built-in methods. Do not mutate input params.
|
|
171
|
+
* `hint` is opaque to shared stages; consumed by this language's `selectDispatch`.
|
|
172
|
+
*
|
|
173
|
+
* Ruby example: bare `serialize` in `Account#call_serialize` →
|
|
174
|
+
* `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
|
|
175
|
+
* receiverSource: 'implicit-self', hint: 'instance' }`
|
|
176
|
+
*
|
|
177
|
+
* @see call-types.ts § ImplicitReceiverOverride
|
|
178
|
+
* @see selectDispatch (stage 4, reads the hint)
|
|
179
|
+
*
|
|
180
|
+
* Default: undefined (no implicit-receiver inference).
|
|
181
|
+
*/
|
|
182
|
+
readonly inferImplicitReceiver?: (params: {
|
|
183
|
+
readonly calledName: string;
|
|
184
|
+
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
|
185
|
+
readonly receiverName: string | undefined;
|
|
186
|
+
readonly receiverTypeName: string | undefined;
|
|
187
|
+
readonly callNode: SyntaxNode;
|
|
188
|
+
readonly filePath: string;
|
|
189
|
+
}) => ImplicitReceiverOverride | null;
|
|
190
|
+
/**
|
|
191
|
+
* DAG stage 4 hook: decide dispatch strategy (primary path, fallback, MRO view).
|
|
192
|
+
*
|
|
193
|
+
* Runs after stage 3. Return a `DispatchDecision` to override shared defaults;
|
|
194
|
+
* return null to use `defaultDispatchDecision` (constructor→`'constructor'`,
|
|
195
|
+
* member→`'owner-scoped'`, free→`'free'`). Most languages return null.
|
|
196
|
+
*
|
|
197
|
+
* The hook is responsible for its own gating. `ancestryView` only affects
|
|
198
|
+
* `'ruby-mixin'` strategy. Singleton-ancestry miss NEVER falls through to
|
|
199
|
+
* file-scoped fallback in stage 5 (enforced in resolveCallTarget).
|
|
200
|
+
*
|
|
201
|
+
* Ruby examples:
|
|
202
|
+
* - `receiverSource='implicit-self', hint='instance'` →
|
|
203
|
+
* `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}`
|
|
204
|
+
* - `receiverSource='class-as-receiver'` →
|
|
205
|
+
* `{primary: 'owner-scoped', ancestryView: 'singleton'}` (miss null-routes)
|
|
206
|
+
* - `receiverSource='implicit-self', hint='singleton'` →
|
|
207
|
+
* `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'singleton'}`
|
|
208
|
+
*
|
|
209
|
+
* @see call-types.ts § DispatchDecision
|
|
210
|
+
* @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
|
|
211
|
+
*
|
|
212
|
+
* Default: undefined (use `defaultDispatchDecision`).
|
|
213
|
+
*/
|
|
214
|
+
readonly selectDispatch?: (params: {
|
|
215
|
+
readonly calledName: string;
|
|
216
|
+
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
|
217
|
+
readonly receiverName: string | undefined;
|
|
218
|
+
readonly receiverTypeName: string | undefined;
|
|
219
|
+
readonly receiverSource: ReceiverEnriched['receiverSource'];
|
|
220
|
+
readonly hint: string | undefined;
|
|
221
|
+
}) => DispatchDecision | null;
|
|
154
222
|
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
|
155
223
|
* Default: undefined (no language-specific filtering). */
|
|
156
224
|
readonly builtInNames?: ReadonlySet<string>;
|
|
@@ -35,6 +35,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|
|
35
35
|
import { cVariableConfig, cppVariableConfig } from '../variable-extractors/configs/c-cpp.js';
|
|
36
36
|
import { createCallExtractor } from '../call-extractors/generic.js';
|
|
37
37
|
import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js';
|
|
38
|
+
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
|
38
39
|
const C_BUILT_INS = new Set([
|
|
39
40
|
'printf',
|
|
40
41
|
'fprintf',
|
|
@@ -301,6 +302,7 @@ export const cProvider = defineLanguage({
|
|
|
301
302
|
}),
|
|
302
303
|
variableExtractor: createVariableExtractor(cVariableConfig),
|
|
303
304
|
classExtractor: cClassExtractor,
|
|
305
|
+
heritageExtractor: createHeritageExtractor(SupportedLanguages.C),
|
|
304
306
|
labelOverride: cppLabelOverride,
|
|
305
307
|
builtInNames: C_BUILT_INS,
|
|
306
308
|
});
|
|
@@ -321,6 +323,7 @@ export const cppProvider = defineLanguage({
|
|
|
321
323
|
}),
|
|
322
324
|
variableExtractor: createVariableExtractor(cppVariableConfig),
|
|
323
325
|
classExtractor: cppClassExtractor,
|
|
326
|
+
heritageExtractor: createHeritageExtractor(SupportedLanguages.CPlusPlus),
|
|
324
327
|
labelOverride: cppLabelOverride,
|
|
325
328
|
builtInNames: C_BUILT_INS,
|
|
326
329
|
});
|
|
@@ -23,6 +23,7 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
|
|
|
23
23
|
import { csharpMethodConfig } from '../method-extractors/configs/csharp.js';
|
|
24
24
|
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|
25
25
|
import { csharpVariableConfig } from '../variable-extractors/configs/csharp.js';
|
|
26
|
+
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
|
26
27
|
const BUILT_INS = new Set([
|
|
27
28
|
'Console',
|
|
28
29
|
'WriteLine',
|
|
@@ -132,5 +133,6 @@ export const csharpProvider = defineLanguage({
|
|
|
132
133
|
methodExtractor: createMethodExtractor(csharpMethodConfig),
|
|
133
134
|
variableExtractor: createVariableExtractor(csharpVariableConfig),
|
|
134
135
|
classExtractor: createClassExtractor(csharpClassConfig),
|
|
136
|
+
heritageExtractor: createHeritageExtractor(SupportedLanguages.CSharp),
|
|
135
137
|
builtInNames: BUILT_INS,
|
|
136
138
|
});
|