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
|
@@ -233,35 +233,112 @@ const buildParentMapFromHeritage = (startNodeId, heritageMap) => {
|
|
|
233
233
|
// MRO-aware method lookup
|
|
234
234
|
// ---------------------------------------------------------------------------
|
|
235
235
|
/**
|
|
236
|
-
*
|
|
237
|
-
* when the method isn't found on the direct owner.
|
|
236
|
+
* DAG stage 5 helper: look up a method on an owner class via MRO walk.
|
|
238
237
|
*
|
|
239
|
-
*
|
|
240
|
-
* -
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* so BFS order is equivalent to leftmost-base semantics
|
|
244
|
-
* - `c3`: C3-linearized ancestor order, first match wins (Python)
|
|
245
|
-
* - `implements-split`: BFS ancestor walk, first match wins (Java/C#) —
|
|
246
|
-
* full ambiguity detection for multiple interface defaults
|
|
247
|
-
* is handled by computeMRO at graph level
|
|
248
|
-
* - `qualified-syntax`: No auto-resolution (Rust) — returns undefined
|
|
238
|
+
* Low-level resolver; no dependency on SymbolTable, language registry, or
|
|
239
|
+
* resolution-context (keeps model/ layer free of cross-layer imports).
|
|
240
|
+
* All strategies respect `argCount` for overload narrowing.
|
|
241
|
+
* `ancestryOverride` replaces the default walk; caller must compute it correctly.
|
|
249
242
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
243
|
+
* Strategy summary (full docs in gitnexus-shared/mro-strategy.ts):
|
|
244
|
+
* - `first-wins` / `leftmost-base` / `implements-split`: BFS, first match wins.
|
|
245
|
+
* - `c3`: C3-linearized order; falls back to BFS on cycle/inconsistency.
|
|
246
|
+
* - `qualified-syntax`: returns undefined immediately (Rust requires explicit syntax).
|
|
247
|
+
* - `ruby-mixin`: kind-aware walk — see inline comments below.
|
|
252
248
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* registry, or resolution-context, which keeps the `model/` module free of
|
|
256
|
-
* cross-layer imports. Callers derive the strategy from their language
|
|
257
|
-
* provider before invoking this function.
|
|
249
|
+
* Internal API: exported for call-processor resolvers and tests.
|
|
250
|
+
* External callers should use resolveMemberCall instead.
|
|
258
251
|
*
|
|
259
|
-
* @
|
|
260
|
-
*
|
|
261
|
-
* outside `core/ingestion/` should use the higher-level resolvers in
|
|
262
|
-
* call-processor.ts instead of depending on this function.
|
|
252
|
+
* @see gitnexus-shared/mro-strategy.ts § 'ruby-mixin'
|
|
253
|
+
* @see call-processor.ts § resolveMemberCall
|
|
263
254
|
*/
|
|
264
|
-
export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap, model, strategy, argCount
|
|
255
|
+
export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap, model, strategy, argCount,
|
|
256
|
+
/**
|
|
257
|
+
* Optional pre-computed ancestry list. When provided, overrides the default
|
|
258
|
+
* per-strategy ancestry source. Primarily used by Ruby singleton dispatch:
|
|
259
|
+
* the caller supplies `heritageMap.getSingletonAncestry(ownerNodeId)` as
|
|
260
|
+
* node-id array so this walker resolves against `extend` providers only.
|
|
261
|
+
*
|
|
262
|
+
* For `ruby-mixin` strategy, passing an override switches the walker into
|
|
263
|
+
* a no-prepend-no-direct linear scan (the caller has already decided the
|
|
264
|
+
* order), which is the correct semantics for singleton dispatch.
|
|
265
|
+
*/
|
|
266
|
+
ancestryOverride) => {
|
|
267
|
+
// ── Ruby mixin strategy ───────────────────────────────────────────
|
|
268
|
+
// Kind-aware walk — does NOT short-circuit on direct owner first (prepend beats direct).
|
|
269
|
+
// Instance dispatch: prepend (reverse) → direct → include (reverse) → transitive BFS.
|
|
270
|
+
// Singleton dispatch: caller supplies ancestryOverride (extend providers only);
|
|
271
|
+
// simple left-to-right scan. Miss NEVER falls through to file-scoped fallback.
|
|
272
|
+
// See gitnexus-shared/mro-strategy.ts § 'ruby-mixin' for full strategy docs.
|
|
273
|
+
if (strategy === 'ruby-mixin') {
|
|
274
|
+
if (ancestryOverride) {
|
|
275
|
+
// Singleton dispatch: scan pre-computed ancestry only. Miss null-routes.
|
|
276
|
+
for (const ancestorId of ancestryOverride) {
|
|
277
|
+
const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
|
|
278
|
+
if (method)
|
|
279
|
+
return method;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
// Instance dispatch — kind-aware walk per the pseudocode above.
|
|
284
|
+
const instanceEntries = heritageMap.getInstanceAncestry(ownerNodeId);
|
|
285
|
+
// Partition into prepend parents vs other parents (extends / include /
|
|
286
|
+
// implements / trait-impl), preserving declaration order within each.
|
|
287
|
+
const prependParents = [];
|
|
288
|
+
const otherParents = [];
|
|
289
|
+
for (const e of instanceEntries) {
|
|
290
|
+
if (e.kind === 'prepend')
|
|
291
|
+
prependParents.push(e.parentId);
|
|
292
|
+
else
|
|
293
|
+
otherParents.push(e.parentId);
|
|
294
|
+
}
|
|
295
|
+
// Step 1: Walk prepend parents in REVERSE declaration order (last-prepended wins).
|
|
296
|
+
for (let i = prependParents.length - 1; i >= 0; i--) {
|
|
297
|
+
const method = model.methods.lookupMethodByOwner(prependParents[i], methodName, argCount);
|
|
298
|
+
if (method)
|
|
299
|
+
return method;
|
|
300
|
+
}
|
|
301
|
+
// Step 2: Direct owner lookup (the class's own method).
|
|
302
|
+
// This is the only difference from other strategies — prepend beats direct.
|
|
303
|
+
const direct = model.methods.lookupMethodByOwner(ownerNodeId, methodName, argCount);
|
|
304
|
+
if (direct)
|
|
305
|
+
return direct;
|
|
306
|
+
// Step 3: Walk extends + include parents in REVERSE declaration order.
|
|
307
|
+
// (Ruby `include A; include B` puts B ahead of A in MRO.)
|
|
308
|
+
for (let i = otherParents.length - 1; i >= 0; i--) {
|
|
309
|
+
const method = model.methods.lookupMethodByOwner(otherParents[i], methodName, argCount);
|
|
310
|
+
if (method)
|
|
311
|
+
return method;
|
|
312
|
+
}
|
|
313
|
+
// Step 4: Transitive ancestors (a mixin that itself mixes in another module).
|
|
314
|
+
// Fall back to the BFS ancestor walk for depth > 1. Order is best-effort;
|
|
315
|
+
// Ruby's actual MRO for transitive mixins is rare and under-specified
|
|
316
|
+
// (documented in architecture docs as deferred work).
|
|
317
|
+
//
|
|
318
|
+
// O(1) skip-check via Sets:
|
|
319
|
+
// - `walkedDirect` covers parents already visited in steps 1-3.
|
|
320
|
+
// - `singletonOnly` covers direct `extend` providers: they belong to
|
|
321
|
+
// the singleton MRO and must NEVER appear in instance dispatch.
|
|
322
|
+
// Building Sets once before the BFS loop avoids O(n²) `Array.includes`
|
|
323
|
+
// on large mixin hierarchies.
|
|
324
|
+
const walkedDirect = new Set(prependParents);
|
|
325
|
+
for (const id of otherParents)
|
|
326
|
+
walkedDirect.add(id);
|
|
327
|
+
const singletonOnly = new Set(heritageMap.getSingletonAncestry(ownerNodeId).map((e) => e.parentId));
|
|
328
|
+
for (const ancestorId of heritageMap.getAncestors(ownerNodeId)) {
|
|
329
|
+
if (ancestorId === ownerNodeId)
|
|
330
|
+
continue;
|
|
331
|
+
if (walkedDirect.has(ancestorId))
|
|
332
|
+
continue;
|
|
333
|
+
if (singletonOnly.has(ancestorId))
|
|
334
|
+
continue;
|
|
335
|
+
const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
|
|
336
|
+
if (method)
|
|
337
|
+
return method;
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
// ── Non-Ruby strategies: direct-owner-first short-circuit ─────────
|
|
265
342
|
// Direct lookup first (child override — no walk needed).
|
|
266
343
|
// argCount is threaded through so arity-differing overloads on the direct
|
|
267
344
|
// owner can be disambiguated before the MRO walk starts.
|
|
@@ -274,7 +351,10 @@ export const lookupMethodByOwnerWithMRO = (ownerNodeId, methodName, heritageMap,
|
|
|
274
351
|
// Determine ancestor walk order based on MRO strategy.
|
|
275
352
|
// readonly to accept the cached (frozen) c3 linearization without copying.
|
|
276
353
|
let ancestors;
|
|
277
|
-
if (
|
|
354
|
+
if (ancestryOverride) {
|
|
355
|
+
ancestors = ancestryOverride;
|
|
356
|
+
}
|
|
357
|
+
else if (strategy === 'c3') {
|
|
278
358
|
// C3 linearization (memoized per HeritageMap
|
|
279
359
|
// so repeated calls for the same owner within an ingestion run reuse the
|
|
280
360
|
// linearization instead of rebuilding the parent map and re-running C3).
|
|
@@ -43,5 +43,6 @@ export declare function runChunkedParseAndResolve(graph: KnowledgeGraph, scanned
|
|
|
43
43
|
allORMQueries: ExtractedORMQuery[];
|
|
44
44
|
bindingAccumulator: BindingAccumulator;
|
|
45
45
|
resolutionContext: ReturnType<typeof createResolutionContext>;
|
|
46
|
+
usedWorkerPool: boolean;
|
|
46
47
|
}>;
|
|
47
48
|
export {};
|
|
@@ -98,9 +98,11 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
98
98
|
message: `Parsing ${totalParseable} files in ${numChunks} chunk${numChunks !== 1 ? 's' : ''}...`,
|
|
99
99
|
stats: { filesProcessed: 0, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
100
100
|
});
|
|
101
|
-
// Don't spawn workers for tiny repos — overhead exceeds benefit
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// Don't spawn workers for tiny repos — overhead exceeds benefit.
|
|
102
|
+
// Test suites may lower the thresholds via `options.workerThresholdsForTest`
|
|
103
|
+
// to exercise the worker-pool path with small fixtures; see PipelineOptions.
|
|
104
|
+
const MIN_FILES_FOR_WORKERS = options?.workerThresholdsForTest?.minFiles ?? 15;
|
|
105
|
+
const MIN_BYTES_FOR_WORKERS = options?.workerThresholdsForTest?.minBytes ?? 512 * 1024;
|
|
104
106
|
const totalBytes = parseableScanned.reduce((s, f) => s + f.size, 0);
|
|
105
107
|
// Create worker pool once, reuse across chunks
|
|
106
108
|
let workerPool;
|
|
@@ -433,5 +435,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
433
435
|
allORMQueries,
|
|
434
436
|
bindingAccumulator,
|
|
435
437
|
resolutionContext: ctx,
|
|
438
|
+
// Whether a worker pool was actually live for this run. False means the
|
|
439
|
+
// sequential fallback handled every chunk (either due to `skipWorkers`,
|
|
440
|
+
// the file-count/byte thresholds, or a pool-creation failure).
|
|
441
|
+
usedWorkerPool: workerPool !== undefined,
|
|
436
442
|
};
|
|
437
443
|
}
|
|
@@ -45,5 +45,12 @@ export interface ParseOutput {
|
|
|
45
45
|
readonly allPathSet: ReadonlySet<string>;
|
|
46
46
|
/** Pass-through: total file count for progress reporting. */
|
|
47
47
|
totalFiles: number;
|
|
48
|
+
/**
|
|
49
|
+
* True if the parse phase spawned a live worker pool for this run.
|
|
50
|
+
* False means every chunk ran through the sequential fallback (skipWorkers,
|
|
51
|
+
* thresholds not met, or pool-creation failure). Primarily a test affordance:
|
|
52
|
+
* see `PipelineOptions.workerThresholdsForTest`.
|
|
53
|
+
*/
|
|
54
|
+
readonly usedWorkerPool: boolean;
|
|
48
55
|
}
|
|
49
56
|
export declare const parsePhase: PipelinePhase<ParseOutput>;
|
|
@@ -21,5 +21,16 @@ export interface PipelineOptions {
|
|
|
21
21
|
skipGraphPhases?: boolean;
|
|
22
22
|
/** Force sequential parsing (no worker pool). Useful for testing the sequential path. */
|
|
23
23
|
skipWorkers?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* @internal Test-only override for worker-pool gating thresholds.
|
|
26
|
+
* When unset, production defaults apply (15 files OR 512 KB total bytes).
|
|
27
|
+
* Setting either field lowers the corresponding threshold so small test
|
|
28
|
+
* fixtures can still exercise the worker-pool path. Do not use from
|
|
29
|
+
* production call sites.
|
|
30
|
+
*/
|
|
31
|
+
workerThresholdsForTest?: {
|
|
32
|
+
minFiles?: number;
|
|
33
|
+
minBytes?: number;
|
|
34
|
+
};
|
|
24
35
|
}
|
|
25
36
|
export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void, options?: PipelineOptions) => Promise<PipelineResult>;
|
|
@@ -58,7 +58,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
58
58
|
pipelineStart,
|
|
59
59
|
});
|
|
60
60
|
// Extract final results for the PipelineResult contract
|
|
61
|
-
const { totalFiles } = getPhaseOutput(results, 'parse');
|
|
61
|
+
const { totalFiles, usedWorkerPool } = getPhaseOutput(results, 'parse');
|
|
62
62
|
let communityResult;
|
|
63
63
|
let processResult;
|
|
64
64
|
if (!options?.skipGraphPhases) {
|
|
@@ -77,5 +77,12 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
77
77
|
nodesCreated: graph.nodeCount,
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
graph,
|
|
82
|
+
repoPath,
|
|
83
|
+
totalFileCount: totalFiles,
|
|
84
|
+
communityResult,
|
|
85
|
+
processResult,
|
|
86
|
+
usedWorkerPool,
|
|
87
|
+
};
|
|
81
88
|
};
|
|
@@ -141,7 +141,14 @@ export const CONTAINER_TYPE_TO_LABEL = {
|
|
|
141
141
|
mixin_declaration: 'Mixin',
|
|
142
142
|
extension_declaration: 'Extension',
|
|
143
143
|
class: 'Class',
|
|
144
|
-
module
|
|
144
|
+
// Ruby `module` declarations map to `Trait` so they participate in the
|
|
145
|
+
// class-like type registry used by `lookupClassByName` / `buildHeritageMap`.
|
|
146
|
+
// This lets `include` / `extend` / `prepend` mixin heritage resolve to
|
|
147
|
+
// the providing module. Safe for non-Ruby languages: the only supported
|
|
148
|
+
// grammar that uses the bare `module` AST node type as a container is
|
|
149
|
+
// Ruby (Rust uses `mod_item`). Any new language adding a `module` node
|
|
150
|
+
// type must explicitly reclassify here.
|
|
151
|
+
module: 'Trait',
|
|
145
152
|
singleton_class: 'Class', // Ruby: class << self inherits enclosing class name
|
|
146
153
|
object_declaration: 'Class',
|
|
147
154
|
companion_object: 'Class',
|
|
@@ -177,8 +184,18 @@ export function getLabelFromCaptures(captureMap, provider) {
|
|
|
177
184
|
return 'Enum';
|
|
178
185
|
if (captureMap['definition.namespace'])
|
|
179
186
|
return 'Namespace';
|
|
180
|
-
if (captureMap['definition.module'])
|
|
187
|
+
if (captureMap['definition.module']) {
|
|
188
|
+
// Let providers reclassify module captures (e.g. Ruby remaps `Module`→`Trait`
|
|
189
|
+
// so mixin heritage resolves through `lookupClassByName`). Returning null
|
|
190
|
+
// from labelOverride means "skip this symbol"; treat it as a no-op here so
|
|
191
|
+
// we keep the default label rather than dropping a real definition.
|
|
192
|
+
if (provider.labelOverride) {
|
|
193
|
+
const override = provider.labelOverride(captureMap['definition.module'], 'Module');
|
|
194
|
+
if (override && override !== 'Module')
|
|
195
|
+
return override;
|
|
196
|
+
}
|
|
181
197
|
return 'Module';
|
|
198
|
+
}
|
|
182
199
|
if (captureMap['definition.trait'])
|
|
183
200
|
return 'Trait';
|
|
184
201
|
if (captureMap['definition.impl'])
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby bare-call self-inference helper.
|
|
3
|
+
*
|
|
4
|
+
* Ruby makes `self` implicit for method calls inside instance and class bodies:
|
|
5
|
+
* `serialize` inside `Account#call_serialize` means `self.serialize`. Other
|
|
6
|
+
* supported languages make the receiver explicit in source (`this.x`, `self.x`),
|
|
7
|
+
* so tree-sitter produces a member call directly. Ruby's bare identifier
|
|
8
|
+
* produces either `callForm === 'free'` or `callForm === undefined` (body_statement
|
|
9
|
+
* identifier captures where the @call node IS the @call.name node), and
|
|
10
|
+
* `resolveFreeCall` does a global tiered name lookup — no MRO walk.
|
|
11
|
+
*
|
|
12
|
+
* This helper is a pure decision function consumed by the Ruby language
|
|
13
|
+
* provider's `inferImplicitReceiver` hook. Shared pipeline code never imports
|
|
14
|
+
* it directly — only `languages/ruby.ts` does.
|
|
15
|
+
*/
|
|
16
|
+
import type { SyntaxNode } from './ast-helpers.js';
|
|
17
|
+
import type { LanguageProvider } from '../language-provider.js';
|
|
18
|
+
/**
|
|
19
|
+
* Rewrite suggestion returned by `maybeRewriteRubyBareCallToSelf`.
|
|
20
|
+
*
|
|
21
|
+
* `callForm` is always `'member'`; `receiverName` is always `'self'`.
|
|
22
|
+
* `dispatchKind` controls the stage-4 ancestry view:
|
|
23
|
+
* - `'instance'` → prepend → direct → include (normal MRO)
|
|
24
|
+
* - `'singleton'` → extend providers only, no file-scoped fallback
|
|
25
|
+
*
|
|
26
|
+
* Consumed by `languages/ruby.ts § inferImplicitReceiver` (wraps into
|
|
27
|
+
* `ImplicitReceiverOverride`; `dispatchKind` becomes the `hint` field).
|
|
28
|
+
*/
|
|
29
|
+
export interface SelfCallRewrite {
|
|
30
|
+
readonly callForm: 'member';
|
|
31
|
+
readonly receiverName: 'self';
|
|
32
|
+
readonly receiverTypeName: string;
|
|
33
|
+
/** `'singleton'` when the enclosing method is `def self.foo` / inside a
|
|
34
|
+
* `singleton_class` body; `'instance'` otherwise. Controls MRO ancestry
|
|
35
|
+
* view selection in stage-4 dispatch. */
|
|
36
|
+
readonly dispatchKind: 'instance' | 'singleton';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pure decision function: should a bare Ruby call be rewritten as `self.method`?
|
|
40
|
+
*
|
|
41
|
+
* Returns a `SelfCallRewrite` when all gates pass; null otherwise.
|
|
42
|
+
* Gates (all required): `callForm` is `'free'` or `undefined`, strategy is
|
|
43
|
+
* `'ruby-mixin'`, `enclosingClassName` is non-null, name is not `'super'`,
|
|
44
|
+
* name is not a built-in.
|
|
45
|
+
*
|
|
46
|
+
* Note: Ruby body-statement identifiers produce `callForm === undefined` because
|
|
47
|
+
* the @call node IS the @call.name node in tree-sitter-ruby.
|
|
48
|
+
*
|
|
49
|
+
* Example: `calledName='serialize'` in `Account` instance method →
|
|
50
|
+
* `{callForm:'member', receiverName:'self', receiverTypeName:'Account', dispatchKind:'instance'}`
|
|
51
|
+
*/
|
|
52
|
+
export declare function maybeRewriteRubyBareCallToSelf(calledName: string, callForm: 'free' | 'member' | 'constructor' | undefined, callNode: SyntaxNode, enclosingClassName: string | null, provider: Pick<LanguageProvider, 'isBuiltInName' | 'mroStrategy'>): SelfCallRewrite | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// gitnexus/src/core/ingestion/utils/ruby-self-call.ts
|
|
2
|
+
/** Maximum parent-walk depth to prevent runaway traversal. */
|
|
3
|
+
const MAX_PARENT_DEPTH = 50;
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if `callNode` is inside a `singleton_method` or `singleton_class`.
|
|
6
|
+
* Stops at `class`/`module` boundary or MAX_PARENT_DEPTH (50) to bound traversal.
|
|
7
|
+
*/
|
|
8
|
+
function isInsideSingletonMethod(callNode) {
|
|
9
|
+
let current = callNode.parent;
|
|
10
|
+
let depth = 0;
|
|
11
|
+
while (current && depth++ < MAX_PARENT_DEPTH) {
|
|
12
|
+
if (current.type === 'singleton_method')
|
|
13
|
+
return true;
|
|
14
|
+
if (current.type === 'singleton_class')
|
|
15
|
+
return true;
|
|
16
|
+
if (current.type === 'class' || current.type === 'module')
|
|
17
|
+
return false;
|
|
18
|
+
current = current.parent;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pure decision function: should a bare Ruby call be rewritten as `self.method`?
|
|
24
|
+
*
|
|
25
|
+
* Returns a `SelfCallRewrite` when all gates pass; null otherwise.
|
|
26
|
+
* Gates (all required): `callForm` is `'free'` or `undefined`, strategy is
|
|
27
|
+
* `'ruby-mixin'`, `enclosingClassName` is non-null, name is not `'super'`,
|
|
28
|
+
* name is not a built-in.
|
|
29
|
+
*
|
|
30
|
+
* Note: Ruby body-statement identifiers produce `callForm === undefined` because
|
|
31
|
+
* the @call node IS the @call.name node in tree-sitter-ruby.
|
|
32
|
+
*
|
|
33
|
+
* Example: `calledName='serialize'` in `Account` instance method →
|
|
34
|
+
* `{callForm:'member', receiverName:'self', receiverTypeName:'Account', dispatchKind:'instance'}`
|
|
35
|
+
*/
|
|
36
|
+
export function maybeRewriteRubyBareCallToSelf(calledName, callForm, callNode, enclosingClassName, provider) {
|
|
37
|
+
// Body-statement bare identifiers produce `callForm === undefined` because
|
|
38
|
+
// the @call node IS the @call.name node in tree-sitter-ruby. Treat both
|
|
39
|
+
// undefined and 'free' as qualifying.
|
|
40
|
+
if (callForm !== 'free' && callForm !== undefined)
|
|
41
|
+
return null;
|
|
42
|
+
if (provider.mroStrategy !== 'ruby-mixin')
|
|
43
|
+
return null;
|
|
44
|
+
if (!enclosingClassName)
|
|
45
|
+
return null;
|
|
46
|
+
if (calledName === 'super')
|
|
47
|
+
return null;
|
|
48
|
+
if (provider.isBuiltInName(calledName))
|
|
49
|
+
return null;
|
|
50
|
+
const dispatchKind = isInsideSingletonMethod(callNode)
|
|
51
|
+
? 'singleton'
|
|
52
|
+
: 'instance';
|
|
53
|
+
return {
|
|
54
|
+
callForm: 'member',
|
|
55
|
+
receiverName: 'self',
|
|
56
|
+
receiverTypeName: enclosingClassName,
|
|
57
|
+
dispatchKind,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1021,33 +1021,36 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1021
1021
|
// Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
|
|
1022
1022
|
// in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
|
|
1023
1023
|
// runs. This pre-pass makes parent class information available for type resolution.
|
|
1024
|
+
const provider = getProvider(language);
|
|
1024
1025
|
const fileParentMap = new Map();
|
|
1025
|
-
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1026
|
+
if (provider.heritageExtractor) {
|
|
1027
|
+
for (const match of matches) {
|
|
1028
|
+
const captureMap = {};
|
|
1029
|
+
for (const c of match.captures) {
|
|
1030
|
+
captureMap[c.name] = c.node;
|
|
1031
|
+
}
|
|
1032
|
+
if (captureMap['heritage.class']) {
|
|
1033
|
+
const heritageItems = provider.heritageExtractor.extract(captureMap, {
|
|
1034
|
+
filePath: file.path,
|
|
1035
|
+
language,
|
|
1036
|
+
});
|
|
1037
|
+
for (const item of heritageItems) {
|
|
1038
|
+
if (item.kind === 'extends') {
|
|
1039
|
+
let parents = fileParentMap.get(item.className);
|
|
1040
|
+
if (!parents) {
|
|
1041
|
+
parents = [];
|
|
1042
|
+
fileParentMap.set(item.className, parents);
|
|
1043
|
+
}
|
|
1044
|
+
if (!parents.includes(item.parentName))
|
|
1045
|
+
parents.push(item.parentName);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1042
1048
|
}
|
|
1043
|
-
if (!parents.includes(parentName))
|
|
1044
|
-
parents.push(parentName);
|
|
1045
1049
|
}
|
|
1046
1050
|
}
|
|
1047
1051
|
// Build per-file type environment + constructor bindings in a single AST walk.
|
|
1048
1052
|
// Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
|
|
1049
1053
|
const parentMap = fileParentMap;
|
|
1050
|
-
const provider = getProvider(language);
|
|
1051
1054
|
const typeEnv = buildTypeEnv(tree, language, {
|
|
1052
1055
|
parentMap,
|
|
1053
1056
|
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
|
@@ -1291,7 +1294,23 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1291
1294
|
// ── Path 2: Generic extraction via @call.name ────────────────
|
|
1292
1295
|
if (callNameNode) {
|
|
1293
1296
|
const calledName = callNameNode.text;
|
|
1294
|
-
//
|
|
1297
|
+
// Check heritage extractor for call-based heritage (e.g., Ruby include/extend/prepend)
|
|
1298
|
+
if (provider.heritageExtractor?.extractFromCall) {
|
|
1299
|
+
const heritageItems = provider.heritageExtractor.extractFromCall(calledName, callNode, { filePath: file.path, language });
|
|
1300
|
+
if (heritageItems !== null) {
|
|
1301
|
+
for (const item of heritageItems) {
|
|
1302
|
+
result.heritage.push({
|
|
1303
|
+
filePath: file.path,
|
|
1304
|
+
className: item.className,
|
|
1305
|
+
parentName: item.parentName,
|
|
1306
|
+
kind: item.kind,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
// Dispatch: route language-specific calls (properties, imports)
|
|
1313
|
+
// Heritage routing is handled by heritageExtractor.extractFromCall above.
|
|
1295
1314
|
const routed = callRouter?.(calledName, captureMap['call']);
|
|
1296
1315
|
if (routed) {
|
|
1297
1316
|
if (routed.kind === 'skip')
|
|
@@ -1304,17 +1323,6 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1304
1323
|
});
|
|
1305
1324
|
continue;
|
|
1306
1325
|
}
|
|
1307
|
-
if (routed.kind === 'heritage') {
|
|
1308
|
-
for (const item of routed.items) {
|
|
1309
|
-
result.heritage.push({
|
|
1310
|
-
filePath: file.path,
|
|
1311
|
-
className: item.enclosingClass,
|
|
1312
|
-
parentName: item.mixinName,
|
|
1313
|
-
kind: item.heritageKind,
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
continue;
|
|
1317
|
-
}
|
|
1318
1326
|
if (routed.kind === 'properties') {
|
|
1319
1327
|
const propEnclosingInfo = cachedFindEnclosingClassInfo(captureMap['call'], file.path, provider.resolveEnclosingOwner);
|
|
1320
1328
|
const propEnclosingClassId = propEnclosingInfo?.classId ?? null;
|
|
@@ -1453,40 +1461,29 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1453
1461
|
}
|
|
1454
1462
|
continue;
|
|
1455
1463
|
}
|
|
1456
|
-
// Extract heritage (extends/implements)
|
|
1464
|
+
// Extract heritage (extends/implements) via provider heritage extractor
|
|
1457
1465
|
if (captureMap['heritage.class']) {
|
|
1458
|
-
if (
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
const isNamedField = fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name');
|
|
1465
|
-
if (!isNamedField) {
|
|
1466
|
+
if (provider.heritageExtractor) {
|
|
1467
|
+
const heritageItems = provider.heritageExtractor.extract(captureMap, {
|
|
1468
|
+
filePath: file.path,
|
|
1469
|
+
language,
|
|
1470
|
+
});
|
|
1471
|
+
for (const item of heritageItems) {
|
|
1466
1472
|
result.heritage.push({
|
|
1467
1473
|
filePath: file.path,
|
|
1468
|
-
className:
|
|
1469
|
-
parentName:
|
|
1470
|
-
kind:
|
|
1474
|
+
className: item.className,
|
|
1475
|
+
parentName: item.parentName,
|
|
1476
|
+
kind: item.kind,
|
|
1471
1477
|
});
|
|
1472
1478
|
}
|
|
1479
|
+
// When the extractor consumes the match, skip symbol processing below.
|
|
1480
|
+
if (heritageItems.length > 0) {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1473
1483
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
className: captureMap['heritage.class'].text,
|
|
1478
|
-
parentName: captureMap['heritage.implements'].text,
|
|
1479
|
-
kind: 'implements',
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
if (captureMap['heritage.trait']) {
|
|
1483
|
-
result.heritage.push({
|
|
1484
|
-
filePath: file.path,
|
|
1485
|
-
className: captureMap['heritage.class'].text,
|
|
1486
|
-
parentName: captureMap['heritage.trait'].text,
|
|
1487
|
-
kind: 'trait-impl',
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1484
|
+
// Fallback: the extractor returned [] (or is absent), but the match still
|
|
1485
|
+
// carries a heritage-specific capture. The match belongs to a heritage
|
|
1486
|
+
// clause and must not fall through to generic symbol processing.
|
|
1490
1487
|
if (captureMap['heritage.extends'] ||
|
|
1491
1488
|
captureMap['heritage.implements'] ||
|
|
1492
1489
|
captureMap['heritage.trait']) {
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -9,4 +9,10 @@ export interface PipelineResult {
|
|
|
9
9
|
totalFileCount: number;
|
|
10
10
|
communityResult?: CommunityDetectionResult;
|
|
11
11
|
processResult?: ProcessDetectionResult;
|
|
12
|
+
/**
|
|
13
|
+
* True if the parse phase spawned a worker pool for this run. False means
|
|
14
|
+
* the sequential fallback handled every chunk. Primarily a test affordance
|
|
15
|
+
* so regression suites can prove which path executed.
|
|
16
|
+
*/
|
|
17
|
+
usedWorkerPool: boolean;
|
|
12
18
|
}
|
package/package.json
CHANGED