Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/c-cpp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// gitnexus/src/core/ingestion/call-extractors/configs/c-cpp.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const cCallConfig: CallExtractionConfig = {
language: SupportedLanguages.C,
};

export const cppCallConfig: CallExtractionConfig = {
language: SupportedLanguages.CPlusPlus,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// gitnexus/src/core/ingestion/call-extractors/configs/csharp.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const csharpCallConfig: CallExtractionConfig = {
language: SupportedLanguages.CSharp,
typeAsReceiverHeuristic: true,
};
8 changes: 8 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/dart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/dart.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const dartCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Dart,
};
8 changes: 8 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/go.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/go.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const goCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Go,
};
59 changes: 59 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/jvm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// gitnexus/src/core/ingestion/call-extractors/configs/jvm.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig, ExtractedCallSite } from '../../call-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';

// ---------------------------------------------------------------------------
// Java method_reference (::) parsing — absorbs call-sites/java.ts
// ---------------------------------------------------------------------------

/**
* Parse Java `method_reference` nodes (`expr::method`, `Type::new`,
* `this::m`, `super::m`).
*/
function parseJavaMethodReference(callNode: SyntaxNode): ExtractedCallSite | null {
if (callNode.type !== 'method_reference') return null;

const recv = callNode.namedChild(0);
if (!recv) return null;

// Type::new → constructor call
for (const c of callNode.children) {
if (c.type === 'new') {
if (recv.type !== 'identifier') return null;
return { calledName: recv.text, callForm: 'constructor' };
}
}

// expr::method → member call with receiver
const rhs = callNode.child(callNode.childCount - 1);
if (!rhs || rhs.type !== 'identifier') return null;
const methodName = rhs.text;

if (recv.type === 'identifier') {
return { calledName: methodName, callForm: 'member', receiverName: recv.text };
}
if (recv.type === 'this') {
return { calledName: methodName, callForm: 'member', receiverName: 'this' };
}
if (recv.type === 'super') {
return { calledName: methodName, callForm: 'member', receiverName: 'super' };
}
return null;
}

// ---------------------------------------------------------------------------
// Configs
// ---------------------------------------------------------------------------

export const javaCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Java,
extractLanguageCallSite: parseJavaMethodReference,
typeAsReceiverHeuristic: true,
};

export const kotlinCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Kotlin,
typeAsReceiverHeuristic: true,
};
8 changes: 8 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/php.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/php.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const phpCallConfig: CallExtractionConfig = {
language: SupportedLanguages.PHP,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/python.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const pythonCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Python,
};
8 changes: 8 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/ruby.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/ruby.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const rubyCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Ruby,
};
8 changes: 8 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/configs/rust.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/rust.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const rustCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Rust,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/swift.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const swiftCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Swift,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// gitnexus/src/core/ingestion/call-extractors/configs/typescript-javascript.ts

import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';

export const typescriptCallConfig: CallExtractionConfig = {
language: SupportedLanguages.TypeScript,
};

export const javascriptCallConfig: CallExtractionConfig = {
language: SupportedLanguages.JavaScript,
};
86 changes: 86 additions & 0 deletions gitnexus/src/core/ingestion/call-extractors/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// gitnexus/src/core/ingestion/call-extractors/generic.ts

/**
* Generic table-driven call extractor factory.
*
* Mirrors method-extractors/generic.ts and field-extractors/generic.ts —
* define a config per language and generate extractors from configs.
*
* The factory converts a declarative {@link CallExtractionConfig} into a
* runtime {@link CallExtractor} whose `extract()` method:
* 1. Tries `config.extractLanguageCallSite(callNode)` for non-standard shapes.
* 2. Falls through to the generic path using shared utilities from
* `utils/call-analysis.ts` (`inferCallForm`, `extractReceiverName`, etc.).
*/

import type { SyntaxNode } from '../utils/ast-helpers.js';
import {
inferCallForm,
extractReceiverName,
extractReceiverNode,
extractMixedChain,
countCallArguments,
} from '../utils/call-analysis.js';
import type { CallExtractor, CallExtractionConfig, ExtractedCallSite } from '../call-types.js';

/**
* Create a CallExtractor from a declarative config.
*/
export function createCallExtractor(config: CallExtractionConfig): CallExtractor {
return {
language: config.language,

extract(callNode: SyntaxNode, callNameNode: SyntaxNode | undefined): ExtractedCallSite | null {
// ── Path 1: Language-specific call site ──────────────────────────
// Non-standard call shapes (e.g. Java `::` method references) are
// handled entirely by the config hook. When it returns a result,
// the generic path is skipped — no argCount, no mixed chain.
//
// Note: `extractLanguageCallSite` is called on every `extract()`
// invocation — both `extract(callNode, undefined)` (parse-worker
// Path 1) and `extract(callNode, callNameNode)` (Path 2).
// Language hooks must therefore be idempotent and cheap (e.g. a
// single node-type check).
if (config.extractLanguageCallSite) {
const seed = config.extractLanguageCallSite(callNode);
if (seed) {
return {
...seed,
...(config.typeAsReceiverHeuristic ? { typeAsReceiverHeuristic: true } : {}),
};
}
}

// ── Path 2: Generic extraction via @call.name ────────────────────
if (!callNameNode) return null;

const calledName = callNameNode.text;
const callForm = inferCallForm(callNode, callNameNode);
let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
let receiverMixedChain: ExtractedCallSite['receiverMixedChain'];

// When the receiver is a complex expression (call chain, field chain,
// or mixed), extractReceiverName returns undefined. Walk the receiver
// node to build a unified mixed chain for deferred resolution.
if (callForm === 'member' && receiverName === undefined) {
const receiverNode = extractReceiverNode(callNameNode);
if (receiverNode) {
const extracted = extractMixedChain(receiverNode);
if (extracted && extracted.chain.length > 0) {
receiverMixedChain = extracted.chain;
receiverName = extracted.baseReceiverName;
}
}
}

return {
calledName,
...(callForm !== undefined ? { callForm } : {}),
...(receiverName !== undefined ? { receiverName } : {}),
argCount: countCallArguments(callNode),
...(receiverMixedChain !== undefined ? { receiverMixedChain } : {}),
...(config.typeAsReceiverHeuristic ? { typeAsReceiverHeuristic: true } : {}),
};
},
};
}
126 changes: 65 additions & 61 deletions gitnexus/src/core/ingestion/call-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import { extractTemplateComponents } from './vue-sfc-extractor.js';
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
import type { LiteralTypeInferrer } from './type-extractors/types.js';
import type { SyntaxNode } from './utils/ast-helpers.js';
import { extractParsedCallSite } from './call-sites/extract-language-call-site.js';
import { lookupMethodByOwnerWithMRO } from './model/resolve.js';

/** Per-file resolved type bindings for exported symbols.
Expand Down Expand Up @@ -910,74 +909,79 @@ export const processCalls = async (
if (!captureMap['call']) return;

const callNode = captureMap['call'];
const languageSeed = extractParsedCallSite(language, callNode);
if (languageSeed) {
if (provider.isBuiltInName(languageSeed.calledName)) return;

const sourceId =
findEnclosingFunction(callNode, file.path, ctx, provider) ||
generateId('File', file.path);
const receiverName =
languageSeed.callForm === 'member' ? languageSeed.receiverName : undefined;
let receiverTypeName =
receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
const callExtractor = provider.callExtractor;

// ── Language-specific call site (e.g. Java :: method references) ──
if (callExtractor) {
const langCallSite = callExtractor.extract(callNode, undefined);
if (langCallSite) {
if (provider.isBuiltInName(langCallSite.calledName)) return;

const sourceId =
findEnclosingFunction(callNode, file.path, ctx, provider) ||
generateId('File', file.path);
const receiverName =
langCallSite.callForm === 'member' ? langCallSite.receiverName : undefined;
let receiverTypeName =
receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;

if (
receiverName !== undefined &&
receiverTypeName === undefined &&
languageSeed.callForm === 'member' &&
(language === 'java' || language === 'csharp' || language === 'kotlin')
) {
const c0 = receiverName.charCodeAt(0);
if (c0 >= 65 && c0 <= 90) receiverTypeName = receiverName;
}

const resolved = resolveCallTarget(
{
calledName: languageSeed.calledName,
callForm: languageSeed.callForm,
...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
...(receiverName !== undefined ? { receiverName } : {}),
},
file.path,
ctx,
undefined,
widenCache,
undefined,
heritageMap,
);

if (!resolved) return;
graph.addRelationship({
id: generateId('CALLS', `${sourceId}:${languageSeed.calledName}->${resolved.nodeId}`),
sourceId,
targetId: resolved.nodeId,
type: 'CALLS',
confidence: resolved.confidence,
reason: resolved.reason,
});
if (
langCallSite.typeAsReceiverHeuristic &&
receiverName !== undefined &&
receiverTypeName === undefined &&
langCallSite.callForm === 'member'
) {
const c0 = receiverName.charCodeAt(0);
if (c0 >= 65 && c0 <= 90) receiverTypeName = receiverName;
}

if (heritageMap && languageSeed.callForm === 'member' && receiverTypeName) {
const implTargets = findInterfaceDispatchTargets(
languageSeed.calledName,
receiverTypeName,
const resolved = resolveCallTarget(
{
calledName: langCallSite.calledName,
callForm: langCallSite.callForm,
...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
...(receiverName !== undefined ? { receiverName } : {}),
},
file.path,
ctx,
undefined,
widenCache,
undefined,
heritageMap,
resolved.nodeId,
);
for (const impl of implTargets) {
graph.addRelationship({
id: generateId('CALLS', `${sourceId}:${languageSeed.calledName}->${impl.nodeId}`),
sourceId,
targetId: impl.nodeId,
type: 'CALLS',
confidence: impl.confidence,
reason: impl.reason,
});

if (!resolved) return;
graph.addRelationship({
id: generateId('CALLS', `${sourceId}:${langCallSite.calledName}->${resolved.nodeId}`),
sourceId,
targetId: resolved.nodeId,
type: 'CALLS',
confidence: resolved.confidence,
reason: resolved.reason,
});

if (heritageMap && langCallSite.callForm === 'member' && receiverTypeName) {
const implTargets = findInterfaceDispatchTargets(
langCallSite.calledName,
receiverTypeName,
file.path,
ctx,
heritageMap,
resolved.nodeId,
);
for (const impl of implTargets) {
graph.addRelationship({
id: generateId('CALLS', `${sourceId}:${langCallSite.calledName}->${impl.nodeId}`),
sourceId,
targetId: impl.nodeId,
type: 'CALLS',
confidence: impl.confidence,
reason: impl.reason,
});
}
}
return;
}
return;
}

const nameNode = captureMap['call.name'];
Expand Down
Loading
Loading