feat: Implement virtual dispatch and overload disambiguation enhancements

- Updated AGENTS.md and CLAUDE.md to reflect new indexing metrics.
- Enhanced call-processor.ts to support cross-file inheritance tracking and improved virtual dispatch resolution.
- Added support for TypeScript overload signatures in tree-sitter queries.
- Improved type extraction for C++, C#, and Kotlin to handle smart pointers and constructor types.
- Introduced inferLiteralType for overload disambiguation across multiple languages.
- Added tests for C++ smart pointer dispatch and Kotlin virtual dispatch scenarios.
- Updated type-resolution-roadmap.md to reflect completion of phases P.1 to P.3 and outline future work on covariant return types.
This commit is contained in:
Gergo Magyar 2026-03-20 07:23:14 +00:00
parent d4c5e7e253
commit 0004aeeac8
20 changed files with 405 additions and 62 deletions

View file

@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (2174 symbols, 5218 relationships, 166 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **GitNexus** (2184 symbols, 5245 relationships, 167 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

View file

@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (2174 symbols, 5218 relationships, 166 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **GitNexus** (2184 symbols, 5245 relationships, 167 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

View file

@ -30,6 +30,7 @@ import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRo
import { callRouters } from './call-routing.js';
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
import { typeConfigs } from './type-extractors/index.js';
import type { LiteralTypeInferrer } from './type-extractors/types.js';
import type { SyntaxNode } from './utils.js';
// Stdlib methods that preserve the receiver's type identity. When TypeEnv already
@ -141,6 +142,12 @@ export const processCalls = async (
const parser = await loadParser();
const collectedHeritage: ExtractedHeritage[] = [];
const pendingWrites: { receiverTypeName: string; propertyName: string; filePath: string; srcId: string }[] = [];
// Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf.
// Used as a secondary check when per-file parentMap lacks the relationship — helps
// when the heritage-declaring file is processed before the call site file.
// For remaining cases (reverse file order), the SymbolTable class-type fallback applies.
const globalParentMap = new Map<string, string[]>();
const globalParentSeen = new Map<string, Set<string>>();
const logSkipped = isVerboseIngestionEnabled();
const skippedByLang = logSkipped ? new Map<string, number>() : null;
@ -204,6 +211,17 @@ export const processCalls = async (
}
}
const parentMap: ReadonlyMap<string, readonly string[]> = fileParentMap;
// Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
// Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
for (const [cls, parents] of fileParentMap) {
let global = globalParentMap.get(cls);
let seen = globalParentSeen.get(cls);
if (!global) { global = []; globalParentMap.set(cls, global); }
if (!seen) { seen = new Set(); globalParentSeen.set(cls, seen); }
for (const p of parents) {
if (!seen.has(p)) { seen.add(p); global.push(p); }
}
}
const typeEnv = lang ? buildTypeEnv(tree, lang, { symbolTable: ctx.symbols, parentMap }) : null;
const callRouter = callRouters[language];
@ -329,22 +347,35 @@ export const processCalls = async (
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
// Phase P: virtual dispatch override — when the declared type is a base class but
// the constructor created a known subclass, prefer the more specific type.
// Same-file only (parentMap is per-file from heritage pre-pass).
// Scans constructorTypeMap for entries matching `*\0receiverName` since
// reconstructing the exact scope key from call-processor context is fragile.
// Checks per-file parentMap first, then falls back to globalParentMap for
// cross-file heritage (e.g. Dog extends Animal declared in a different file).
// Reconstructs the exact scope key (funcName@startIndex\0varName) from the
// enclosing function AST node for a correct, O(1) map lookup.
if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) {
// Quick scan for matching receiver name in constructorTypeMap
let ctorType: string | undefined;
for (const [key, val] of typeEnv.constructorTypeMap) {
const nul = key.indexOf('\0');
if (nul >= 0 && key.slice(nul + 1) === receiverName) {
ctorType = val;
break;
// Reconstruct scope key to match constructorTypeMap's scope\0varName format
let scope = '';
let p = callNode.parent;
while (p) {
if (FUNCTION_NODE_TYPES.has(p.type)) {
const { funcName } = extractFunctionName(p);
if (funcName) { scope = `${funcName}@${p.startIndex}`; break; }
}
p = p.parent;
}
const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`);
if (ctorType && ctorType !== receiverTypeName) {
// Verify subclass relationship via same-file parentMap
if (isSubclassOf(ctorType, receiverTypeName, parentMap)) {
// Verify subclass relationship: per-file parentMap first, then cross-file
// globalParentMap, then fall back to SymbolTable class verification.
// The SymbolTable fallback handles cross-file cases where heritage is declared
// in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when
// processing services/App.kt). Since constructorTypeMap only records entries
// when a type annotation AND constructor are both present (val x: Base = Sub()),
// confirming both are class-like types is sufficient — the original code would
// not compile if Sub didn't extend Base.
if (isSubclassOf(ctorType, receiverTypeName, parentMap)
|| isSubclassOf(ctorType, receiverTypeName, globalParentMap)
|| (ctx.symbols.lookupFuzzy(ctorType).some(d => d.type === 'Class' || d.type === 'Struct')
&& ctx.symbols.lookupFuzzy(receiverTypeName).some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
receiverTypeName = ctorType;
}
}
@ -525,7 +556,7 @@ const toResolveResult = (
* Only available on the sequential path (has AST); worker path passes undefined. */
interface OverloadHints {
callNode: SyntaxNode;
inferLiteralType: (node: SyntaxNode) => string | undefined;
inferLiteralType: LiteralTypeInferrer;
}
/**
@ -582,10 +613,12 @@ const tryOverloadDisambiguation = (
for (const arg of argList.namedChildren) {
if (arg.type === 'comment') continue;
// Unwrap argument wrapper nodes before passing to inferLiteralType:
// - Kotlin value_argument: literal is the first (and only) named child
// - C# argument: no named fields — the literal is the only named child
// - Kotlin value_argument: has 'value' field containing the literal
// - C# argument: has 'expression' field (handles named args like `name: "alice"`
// where firstNamedChild would return name_colon instead of the value)
// - Java/others: arg IS the literal directly (no unwrapping needed)
const valueNode = arg.childForFieldName?.('value')
?? arg.childForFieldName?.('expression')
?? (arg.type === 'argument' || arg.type === 'value_argument'
? arg.firstNamedChild ?? arg
: arg);
@ -596,7 +629,10 @@ const tryOverloadDisambiguation = (
if (argTypes.every(t => t === undefined)) return null;
const matched = candidates.filter(c => {
if (!c.parameterTypes) return true; // Keep candidates without type info
// Keep candidates without type info — conservative: partially-annotated codebases
// (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
// If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
if (!c.parameterTypes) return true;
return c.parameterTypes.every((pType, i) => {
if (i >= argTypes.length || !argTypes[i]) return true;
// Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
@ -605,7 +641,15 @@ const tryOverloadDisambiguation = (
});
});
return matched.length === 1 ? matched[0] : null;
if (matched.length === 1) return matched[0];
// Multiple survivors may share the same nodeId (e.g. TypeScript overload signatures +
// implementation body all collide via generateId). Deduplicate by nodeId — if all
// matched candidates resolve to the same graph node, disambiguation succeeded.
if (matched.length > 1) {
const uniqueIds = new Set(matched.map(c => c.nodeId));
if (uniqueIds.size === 1) return matched[0];
}
return null;
};
/**
@ -665,8 +709,8 @@ const resolveCallTarget = (
}
// E. Try overload disambiguation on the narrowed pool
if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
const pool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
const disambiguated = tryOverloadDisambiguation(pool, overloadHints);
const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
if (disambiguated) return toResolveResult(disambiguated, tiered.tier);
}
if (fileFiltered.length > 1 || ownerFiltered.length > 1) return null;

View file

@ -6,7 +6,8 @@ export interface SymbolDefinition {
type: NodeLabel;
parameterCount?: number;
/** Per-parameter type names for overload disambiguation (e.g. ['int', 'String']).
* Only populated for overloading languages (Java, Kotlin, C#, C++). */
* Populated when parameter types are resolvable from AST (any typed language).
* Used for disambiguation in overloading languages (Java, Kotlin, C#, C++). */
parameterTypes?: string[];
/** Raw return type text extracted from AST (e.g. 'User', 'Promise<User>') */
returnType?: string;

View file

@ -19,6 +19,10 @@ export const TYPESCRIPT_QUERIES = `
(function_declaration
name: (identifier) @name) @definition.function
; TypeScript overload signatures (function_signature is a separate node type from function_declaration)
(function_signature
name: (identifier) @name) @definition.function
(method_definition
name: (property_identifier) @name) @definition.method

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from './utils.js';
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, isBuiltInOrNoise } from './utils.js';
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES, CALL_EXPRESSION_TYPES, isBuiltInOrNoise } from './utils.js';
import { SupportedLanguages } from '../../config/supported-languages.js';
import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
import type { ClassNameLookup, ReturnTypeLookup, ForLoopExtractorContext, PendingAssignment } from './type-extractors/types.js';
@ -409,9 +409,12 @@ const createClassDefCache = (symbolTable?: SymbolTable) => {
};
};
/** AST node types representing constructor expressions across languages. */
/** AST node types representing constructor expressions across languages.
* Note: C# also has `implicit_object_creation_expression` (`new()` with type
* inference) which is NOT captured — the type is inferred, not explicit.
* Kotlin constructors use `call_expression` (no `new` keyword) — not detected. */
const CONSTRUCTOR_EXPR_TYPES = new Set([
'new_expression', // TS/JS: new Dog()
'new_expression', // TS/JS/C++: new Dog()
'object_creation_expression', // Java/C#: new Dog()
]);
@ -434,14 +437,19 @@ const extractConstructorTypeName = (node: SyntaxNode, depth = 0): string | undef
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
// Don't descend into nested functions/classes
if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)) continue;
// Don't descend into nested functions/classes or call expressions (prevents
// finding constructor args inside method calls, e.g. processAll(new Dog()))
if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)
|| CALL_EXPRESSION_TYPES.has(child.type)) continue;
const result = extractConstructorTypeName(child, depth + 1);
if (result) return result;
}
return undefined;
};
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
const MAX_MRO_DEPTH = 5;
/** Check if `child` is a subclass of `parent` using the parentMap.
* BFS up from child, depth-limited (5), cycle-safe. */
export const isSubclassOf = (
@ -466,9 +474,6 @@ export const isSubclassOf = (
return false;
};
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
const MAX_MRO_DEPTH = 5;
/** Walk up the parent class chain to find a field or method on an ancestor.
* BFS-like traversal with depth limit and cycle detection. First match wins.
* Used by resolveFieldType and resolveMethodReturnType when direct lookup fails. */
@ -795,7 +800,20 @@ export const buildTypeEnv = (
if (c?.type === 'variable_declaration') { wrapped = c; break; }
}
}
if (wrapped) typeNode = wrapped.childForFieldName('type');
if (wrapped) {
typeNode = wrapped.childForFieldName('type');
// Kotlin: variable_declaration stores the type as user_type / nullable_type
// child rather than a named 'type' field.
if (!typeNode) {
for (let i = 0; i < wrapped.namedChildCount; i++) {
const c = wrapped.namedChild(i);
if (c && (c.type === 'user_type' || c.type === 'nullable_type')) {
typeNode = c;
break;
}
}
}
}
}
if (typeNode) {
const nameNode = node.childForFieldName('name')
@ -837,8 +855,16 @@ export const buildTypeEnv = (
if (keysBefore.has(varName)) continue;
const declaredType = scopeEnv.get(varName);
if (!declaredType) continue;
const ctorType = extractConstructorTypeName(node);
if (ctorType && ctorType !== declaredType) {
const ctorType = extractConstructorTypeName(node)
?? config.detectConstructorType?.(node, classNames);
if (!ctorType || ctorType === declaredType) continue;
// Unwrap wrapper types (e.g., C++ shared_ptr<Animal> → Animal) for an
// accurate isSubclassOf comparison. Language-specific via config hook.
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
const effectiveDeclaredType = (declTypeNode && config.unwrapDeclaredType)
? (config.unwrapDeclaredType(declaredType, declTypeNode) ?? declaredType)
: declaredType;
if (ctorType !== effectiveDeclaredType) {
constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
}
}

View file

@ -1,11 +1,34 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, ForLoopExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, ForLoopExtractor, LiteralTypeInferrer, ConstructorTypeDetector, DeclaredTypeUnwrapper } from './types.js';
import { extractSimpleTypeName, extractVarName, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'declaration',
]);
/** Smart pointer factory function names that create a typed object. */
const SMART_PTR_FACTORIES = new Set([
'make_shared', 'make_unique', 'make_shared_for_overwrite',
]);
/** Smart pointer wrapper type names. When the declared type is a smart pointer,
* the inner template type is extracted for virtual dispatch comparison. */
const SMART_PTR_WRAPPERS = new Set(['shared_ptr', 'unique_ptr', 'weak_ptr']);
/** Extract the first type name from a template_argument_list child.
* Unwraps type_descriptor wrappers common in tree-sitter-cpp ASTs.
* Returns undefined if no template arguments or no type found. */
export const extractFirstTemplateTypeArg = (parentNode: SyntaxNode): string | undefined => {
const templateArgs = parentNode.children.find((c: any) => c.type === 'template_argument_list');
if (!templateArgs?.firstNamedChild) return undefined;
let argNode: any = templateArgs.firstNamedChild;
if (argNode.type === 'type_descriptor') {
const inner = argNode.childForFieldName('type');
if (inner) argNode = inner;
}
return extractSimpleTypeName(argNode) ?? undefined;
};
/** C++: Type x = ...; Type* x; Type& x; */
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
const typeNode = node.childForFieldName('type');
@ -88,6 +111,27 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<str
} else if (func.type === 'identifier') {
const text = func.text;
if (text && classNames.has(text)) env.set(varName, text);
} else {
// auto x = std::make_shared<Dog>() — smart pointer factory via template_function.
// AST: call_expression > function: qualified_identifier > template_function
// or: call_expression > function: template_function (unqualified)
const templateFunc = func.type === 'template_function'
? func
: (func.type === 'qualified_identifier' || func.type === 'scoped_identifier')
? func.namedChildren.find((c: any) => c.type === 'template_function') ?? null
: null;
if (templateFunc) {
const nameNode = templateFunc.firstNamedChild;
if (nameNode) {
const funcName = (nameNode.type === 'qualified_identifier' || nameNode.type === 'scoped_identifier')
? nameNode.lastNamedChild?.text ?? ''
: nameNode.text;
if (SMART_PTR_FACTORIES.has(funcName)) {
const typeName = extractFirstTemplateTypeArg(templateFunc);
if (typeName) env.set(varName, typeName);
}
}
}
}
return;
}
@ -372,7 +416,7 @@ const extractForLoopBinding: ForLoopExtractor = (node, { scopeEnv, declarationTy
};
/** Infer the type of a literal AST node for C++ overload disambiguation. */
const inferLiteralType = (node: SyntaxNode): string | undefined => {
const inferLiteralType: LiteralTypeInferrer = (node) => {
switch (node.type) {
case 'number_literal': {
const t = node.text;
@ -400,6 +444,43 @@ const inferLiteralType = (node: SyntaxNode): string | undefined => {
}
};
/** C++: detect constructor type from smart pointer factory calls (make_shared<Dog>()).
* Extracts the template type argument as the constructor type for virtual dispatch. */
const detectCppConstructorType: ConstructorTypeDetector = (node, classNames) => {
// Navigate to the initializer value in the declaration
const declarator = node.childForFieldName('declarator');
const initDecl = declarator?.type === 'init_declarator' ? declarator : undefined;
if (!initDecl) return undefined;
const value = initDecl.childForFieldName('value');
if (!value || value.type !== 'call_expression') return undefined;
// Check for template_function pattern: make_shared<Dog>()
const func = value.childForFieldName('function');
if (!func || func.type !== 'template_function') return undefined;
// Extract function name (possibly qualified: std::make_shared)
const nameNode = func.firstNamedChild;
if (!nameNode) return undefined;
let funcName: string;
if (nameNode.type === 'qualified_identifier' || nameNode.type === 'scoped_identifier') {
funcName = nameNode.lastNamedChild?.text ?? '';
} else {
funcName = nameNode.text;
}
if (!SMART_PTR_FACTORIES.has(funcName)) return undefined;
// Extract template type argument
return extractFirstTemplateTypeArg(func);
};
/** Unwrap a C++ smart pointer declared type to its inner template type.
* E.g., shared_ptr<Animal> → Animal. Returns the original name if not a smart pointer. */
const unwrapCppDeclaredType: DeclaredTypeUnwrapper = (declaredType, typeNode) => {
if (!SMART_PTR_WRAPPERS.has(declaredType)) return declaredType;
if (typeNode.type !== 'template_type') return declaredType;
return extractFirstTemplateTypeArg(typeNode) ?? declaredType;
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
@ -410,4 +491,6 @@ export const typeConfig: LanguageTypeConfig = {
extractForLoopBinding,
extractPendingAssignment,
inferLiteralType,
detectConstructorType: detectCppConstructorType,
unwrapDeclaredType: unwrapCppDeclaredType,
};

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
import type { ConstructorBindingScanner, ForLoopExtractor, LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, PendingAssignmentExtractor, PatternBindingExtractor, LiteralTypeInferrer } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString, type TypeArgPosition } from './shared.js';
/** Known container property accessors that operate on the container itself (e.g., dict.Keys, dict.Values) */
@ -464,7 +464,7 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
};
/** Infer the type of a literal AST node for C# overload disambiguation. */
const inferLiteralType = (node: SyntaxNode): string | undefined => {
const inferLiteralType: LiteralTypeInferrer = (node) => {
switch (node.type) {
case 'integer_literal':
if (node.text.endsWith('L') || node.text.endsWith('l')) return 'long';

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor, PatternBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ForLoopExtractor, PendingAssignmentExtractor, PatternBindingExtractor, LiteralTypeInferrer, ConstructorTypeDetector } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString, type TypeArgPosition } from './shared.js';
// ── Java ──────────────────────────────────────────────────────────────────
@ -269,7 +269,7 @@ const extractJavaPatternBinding: PatternBindingExtractor = (node) => {
};
/** Infer the type of a literal AST node for Java/Kotlin overload disambiguation. */
const inferJvmLiteralType = (node: SyntaxNode): string | undefined => {
const inferJvmLiteralType: LiteralTypeInferrer = (node) => {
switch (node.type) {
case 'decimal_integer_literal':
case 'integer_literal':
@ -384,26 +384,30 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<s
if (varName && typeName) env.set(varName, typeName);
};
/** Find the constructor callee name in a Kotlin property_declaration's initializer.
* Returns the class name if the callee is a verified class constructor, undefined otherwise. */
const findKotlinConstructorCallee = (node: SyntaxNode, classNames: ClassNameLookup): string | undefined => {
if (node.type !== 'property_declaration') return undefined;
const value = node.childForFieldName('value')
?? findChildByType(node, 'call_expression');
if (!value || value.type !== 'call_expression') return undefined;
const callee = value.firstNamedChild;
if (!callee || callee.type !== 'simple_identifier') return undefined;
const calleeName = callee.text;
if (!calleeName || !classNames.has(calleeName)) return undefined;
return calleeName;
};
/** Kotlin: val user = User() — infer type from call_expression when callee is a known class.
* Kotlin constructors are syntactically identical to function calls, so we verify
* against classNames (which may include cross-file SymbolTable lookups). */
const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
if (node.type !== 'property_declaration') return;
// Skip if there's an explicit type annotation — Tier 0 already handled it
const varDecl = findChildByType(node, 'variable_declaration');
if (varDecl && findChildByType(varDecl, 'user_type')) return;
// Get the initializer value — the call_expression after '='
const value = node.childForFieldName('value')
?? findChildByType(node, 'call_expression');
if (!value || value.type !== 'call_expression') return;
// The callee is the first child of call_expression (simple_identifier for direct calls)
const callee = value.firstNamedChild;
if (!callee || callee.type !== 'simple_identifier') return;
const calleeName = callee.text;
if (!calleeName || !classNames.has(calleeName)) return;
const calleeName = findKotlinConstructorCallee(node, classNames);
if (!calleeName) return;
// Extract the variable name from the variable_declaration inside property_declaration
const nameNode = varDecl
@ -415,6 +419,14 @@ const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: M
if (varName) env.set(varName, calleeName);
};
/** Kotlin: detect constructor type from call_expression in typed declarations.
* Unlike extractKotlinInitializer (which SKIPS typed declarations), this detects
* the constructor type EVEN when a type annotation exists, enabling virtual dispatch
* for patterns like `val a: Animal = Dog()`. */
const detectKotlinConstructorType: ConstructorTypeDetector = (node, classNames) => {
return findKotlinConstructorCallee(node, classNames);
};
/** Kotlin: val x = User(...) — constructor binding for property_declaration with call_expression */
const scanKotlinConstructorBinding: ConstructorBindingScanner = (node) => {
if (node.type !== 'property_declaration') return undefined;
@ -773,4 +785,5 @@ export const kotlinTypeConfig: LanguageTypeConfig = {
extractPendingAssignment: extractKotlinPendingAssignment,
extractPatternBinding: extractKotlinPatternBinding,
inferLiteralType: inferJvmLiteralType,
detectConstructorType: detectKotlinConstructorType,
};

View file

@ -24,6 +24,23 @@ export type ConstructorBindingScanner = (node: SyntaxNode) => { varName: string;
* rather than in AST fields. Returns undefined if no return type can be determined. */
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
/** Infer the type name of a literal AST node for overload disambiguation.
* Returns the canonical type name (e.g. 'int', 'String', 'boolean') or undefined
* for non-literal nodes. Only used when resolveCallTarget has multiple candidates
* with parameterTypes — ~1-3% of call sites. */
export type LiteralTypeInferrer = (node: SyntaxNode) => string | undefined;
/** Detect constructor-style call expressions that don't use `new` keyword.
* Returns the constructor class name if the node's initializer is a constructor call,
* or undefined otherwise. Used for virtual dispatch in languages like Kotlin
* where constructors are syntactically identical to function calls, and C++
* where smart pointer factory functions (make_shared/make_unique) wrap constructors. */
export type ConstructorTypeDetector = (node: SyntaxNode, classNames: ClassNameLookup) => string | undefined;
/** Unwrap a declared type name to its inner type for virtual dispatch comparison.
* E.g., C++ shared_ptr<Animal> → Animal. Returns undefined if no unwrapping applies. */
export type DeclaredTypeUnwrapper = (declaredType: string, typeNode: SyntaxNode) => string | undefined;
/** Narrow lookup interface for resolving a callee name → return type name.
* Backed by SymbolTable.lookupFuzzyCallable; passed via ForLoopExtractorContext.
* Conservative: returns undefined when the callee is ambiguous (0 or 2+ matches). */
@ -149,9 +166,7 @@ export interface LanguageTypeConfig {
* The extractor receives the current scope's resolved bindings (read-only) to look up the
* source variable's type. Returns undefined for non-matching nodes or unknown source types. */
extractPatternBinding?: PatternBindingExtractor;
/** Infer the type name of a literal AST node for overload disambiguation.
* Returns the canonical type name (e.g. 'int', 'String', 'boolean') or undefined
* for non-literal nodes. Only used when resolveCallTarget has multiple candidates
* with parameterTypes — ~1-3% of call sites. */
inferLiteralType?: (node: SyntaxNode) => string | undefined;
inferLiteralType?: LiteralTypeInferrer;
detectConstructorType?: ConstructorTypeDetector;
unwrapDeclaredType?: DeclaredTypeUnwrapper;
}

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, PendingAssignment, ForLoopExtractor, PatternBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, PendingAssignment, ForLoopExtractor, PatternBindingExtractor, LiteralTypeInferrer } from './types.js';
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, type TypeArgPosition } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -597,6 +597,28 @@ const extractPatternBinding: PatternBindingExtractor = (node, scopeEnv, declarat
};
};
/** Infer the type of a literal AST node for TypeScript overload disambiguation. */
const inferTsLiteralType: LiteralTypeInferrer = (node) => {
switch (node.type) {
case 'number':
return 'number';
case 'string':
case 'template_string':
return 'string';
case 'true':
case 'false':
return 'boolean';
case 'null':
return 'null';
case 'undefined':
return 'undefined';
case 'regex':
return 'RegExp';
default:
return undefined;
}
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
@ -609,4 +631,5 @@ export const typeConfig: LanguageTypeConfig = {
extractForLoopBinding,
extractPendingAssignment,
extractPatternBinding,
inferLiteralType: inferTsLiteralType,
};

View file

@ -0,0 +1,17 @@
#include <memory>
#include <string>
class Animal {
public:
virtual std::string speak() { return "..."; }
};
class Dog : public Animal {
public:
std::string speak() override { return "woof"; }
};
void process() {
auto dog = std::make_shared<Dog>();
dog->speak();
}

View file

@ -0,0 +1,5 @@
package models
open class Animal {
open fun speak(): String = "..."
}

View file

@ -0,0 +1,5 @@
package models
class Dog : Animal() {
override fun speak(): String = "woof"
}

View file

@ -0,0 +1,9 @@
package services
import models.Animal
import models.Dog
fun process() {
val animal: Animal = Dog()
animal.speak()
}

View file

@ -0,0 +1,10 @@
function lookup(id: number): string;
function lookup(name: string): string;
function lookup(key: number | string): string {
return String(key);
}
function process() {
lookup(42);
lookup("alice");
}

View file

@ -1088,3 +1088,29 @@ describe('C++ overload disambiguation by parameter types', () => {
expect(lookupCalls.length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// C++ smart pointer virtual dispatch via std::make_shared<T>()
// ---------------------------------------------------------------------------
describe('C++ smart pointer virtual dispatch via make_shared', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-smart-ptr-dispatch'),
() => {},
);
}, 60000);
it('detects Dog and Animal classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('Animal');
expect(getNodesByLabel(result, 'Class')).toContain('Dog');
});
it('emits CALLS edge from process → speak', () => {
const calls = getRelationships(result, 'CALLS');
const speakCall = calls.find(c => c.source === 'process' && c.target === 'speak');
expect(speakCall).toBeDefined();
});
});

View file

@ -1559,3 +1559,29 @@ describe('Kotlin overload disambiguation by parameter types', () => {
expect(lookupCalls.length).toBe(1);
});
});
// ── Phase P: Virtual Dispatch via Constructor Type (cross-file) ──────────
describe('Kotlin virtual dispatch via constructor type (cross-file)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'kotlin-virtual-dispatch'),
() => {},
);
}, 60000);
it('detects Dog class', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Dog');
});
it('resolves animal.speak() to models/Dog.kt via constructor type override', () => {
const calls = getRelationships(result, 'CALLS');
const speakCall = calls.find(c =>
c.source === 'process' && c.target === 'speak' && c.targetFilePath === 'models/Dog.kt',
);
expect(speakCall).toBeDefined();
});
});

View file

@ -4,7 +4,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
runPipelineFromRepo, type PipelineResult,
} from './helpers.js';
@ -2258,3 +2258,35 @@ describe('TypeScript virtual dispatch via constructor type (same-file)', () => {
expect(fetchCalls.length).toBe(1);
});
});
// ── Phase P: Overload Disambiguation via inferLiteralType ────────────────
describe('TypeScript overload disambiguation via inferLiteralType', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'ts-overload-disambiguation'),
() => {},
);
}, 60000);
it('detects lookup function with parameterTypes on graph node', () => {
const functions = getNodesByLabelFull(result, 'Function');
const lookupNodes = functions.filter(f => f.name === 'lookup');
// generateId collision → 1 graph node, first overload's parameterTypes wins
expect(lookupNodes.length).toBeGreaterThanOrEqual(1);
// At least one lookup node has parameterTypes set
const withParamTypes = lookupNodes.filter(n => n.properties.parameterTypes);
expect(withParamTypes.length).toBeGreaterThanOrEqual(1);
});
it('emits CALLS edges from process() → lookup() via overload disambiguation', () => {
const calls = getRelationships(result, 'CALLS');
const lookupCalls = calls.filter(c => c.source === 'process' && c.target === 'lookup');
// Phase 0 (fileIndex stores both overloads) + Phase 2 (literal type matching)
// enables resolution where previously 2 same-arity candidates → null.
// Both calls resolve to same nodeId (ID collision) → 1 CALLS edge after dedup.
expect(lookupCalls.length).toBe(1);
});
});

View file

@ -101,14 +101,14 @@ The goal is not to build a compiler. The goal is to support high-value static an
**Plan:** `docs/plans/2026-03-19-feat-polymorphism-overloading-type-resolution-plan.md`
Four incremental phases:
1. **Parameter type metadata** — extend `SymbolDefinition` with `parameterTypes: string[]` extracted during parsing
2. **Overload disambiguation** — filter overloaded methods by argument literal types at call sites
3. **Constructor-visible virtual dispatch** — `Base b = new Derived(); b.method()` resolves to `Derived#method` when constructor type is a known subclass
1. **Parameter type metadata** — extend `SymbolDefinition` with `parameterTypes: string[]` extracted during parsing — **DELIVERED**
2. **Overload disambiguation** — filter overloaded methods by argument literal types at call sites — **DELIVERED**
3. **Constructor-visible virtual dispatch** — `Base b = new Derived(); b.method()` resolves to `Derived#method` when constructor type is a known subclass — **DELIVERED**
4. **Covariant return type awareness** — prefer child's return type over inherited definition
Languages benefiting: Java, Kotlin, C#, C++, TypeScript (overloading). All OOP languages (virtual dispatch).
**Impact: High | Effort: High**
**Impact: High | Effort: High** (P.1–P.3 delivered; P.4 remains open)
---
@ -165,6 +165,7 @@ Phase P (polymorphism) ───────────┤
│
Phase S (Swift parity) ───────────┘
Phase P.1–P.3 are delivered. P.4 (covariant return types) remains open.
Phase P and Phase S are independent of each other and Phase 14.
Phase 14 benefits from Phase P (better per-file resolution = fewer cross-file gaps).
```
@ -178,6 +179,9 @@ Phase 14 benefits from Phase P (better per-file resolution = fewer cross-file ga
- Assignment chains (copy, callResult, fieldAccess, methodCallResult) → Phase S
- `guard let` narrowing → Phase S
### Kotlin
- Virtual dispatch: `Dog()` uses `call_expression` (no `new` keyword), not detected by `CONSTRUCTOR_EXPR_TYPES`
### All languages
- Cross-file binding propagation → Phase 14