mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-30 06:42:43 +00:00
feat(type-resolution): constructor-visible virtual dispatch via constructorTypeMap
Add constructorTypeMap to buildTypeEnv — populated during walk when a declaration has both a type annotation and a constructor initializer. Add isSubclassOf helper (BFS, depth-5, cycle-safe). In call-processor, consult constructorTypeMap to override receiver type when constructor creates a known subclass (same-file only).
This commit is contained in:
parent
cff6788d05
commit
bc28857898
2 changed files with 105 additions and 1 deletions
|
|
@ -23,7 +23,7 @@ import {
|
|||
extractMixedChain,
|
||||
type MixedChainStep,
|
||||
} from './utils.js';
|
||||
import { buildTypeEnv } from './type-env.js';
|
||||
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
||||
import type { ConstructorBinding } from './type-env.js';
|
||||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
|
||||
|
|
@ -327,6 +327,28 @@ export const processCalls = async (
|
|||
const callForm = inferCallForm(callNode, nameNode);
|
||||
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
||||
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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (ctorType && ctorType !== receiverTypeName) {
|
||||
// Verify subclass relationship via same-file parentMap
|
||||
if (isSubclassOf(ctorType, receiverTypeName, parentMap)) {
|
||||
receiverTypeName = ctorType;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to verified constructor bindings for return type inference
|
||||
if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
|
||||
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ export interface TypeEnvironment {
|
|||
readonly constructorBindings: readonly ConstructorBinding[];
|
||||
/** Raw per-scope type bindings — for testing and debugging. */
|
||||
readonly env: TypeEnv;
|
||||
/** Maps `scope\0varName` → constructor type for virtual dispatch override.
|
||||
* Populated when a variable has BOTH a declared base type AND a more specific
|
||||
* constructor type (e.g., `Animal a = new Dog()` → key maps to 'Dog'). */
|
||||
readonly constructorTypeMap: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -405,6 +409,63 @@ const createClassDefCache = (symbolTable?: SymbolTable) => {
|
|||
};
|
||||
};
|
||||
|
||||
/** AST node types representing constructor expressions across languages. */
|
||||
const CONSTRUCTOR_EXPR_TYPES = new Set([
|
||||
'new_expression', // TS/JS: new Dog()
|
||||
'object_creation_expression', // Java/C#: new Dog()
|
||||
]);
|
||||
|
||||
/** Extract the constructor class name from a declaration node's initializer.
|
||||
* Searches for new_expression / object_creation_expression in the node's subtree.
|
||||
* Returns the class name or undefined if no constructor is found.
|
||||
* Depth-limited to 5 to avoid expensive traversals. */
|
||||
const extractConstructorTypeName = (node: SyntaxNode, depth = 0): string | undefined => {
|
||||
if (depth > 5) return undefined;
|
||||
if (CONSTRUCTOR_EXPR_TYPES.has(node.type)) {
|
||||
// Java/C#: object_creation_expression has 'type' field
|
||||
const typeField = node.childForFieldName('type');
|
||||
if (typeField) return extractSimpleTypeName(typeField);
|
||||
// TS/JS: new_expression has 'constructor' field (but tree-sitter often just has identifier child)
|
||||
const ctorField = node.childForFieldName('constructor');
|
||||
if (ctorField) return extractSimpleTypeName(ctorField);
|
||||
// Fallback: first named child is often the class identifier
|
||||
if (node.firstNamedChild) return extractSimpleTypeName(node.firstNamedChild);
|
||||
}
|
||||
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;
|
||||
const result = extractConstructorTypeName(child, depth + 1);
|
||||
if (result) return result;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Check if `child` is a subclass of `parent` using the parentMap.
|
||||
* BFS up from child, depth-limited (5), cycle-safe. */
|
||||
export const isSubclassOf = (
|
||||
child: string, parent: string,
|
||||
parentMap: ReadonlyMap<string, readonly string[]> | undefined,
|
||||
): boolean => {
|
||||
if (!parentMap || child === parent) return false;
|
||||
const visited = new Set<string>([child]);
|
||||
let current = [child];
|
||||
for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
|
||||
const next: string[] = [];
|
||||
for (const cls of current) {
|
||||
const parents = parentMap.get(cls);
|
||||
if (!parents) continue;
|
||||
for (const p of parents) {
|
||||
if (p === parent) return true;
|
||||
if (!visited.has(p)) { visited.add(p); next.push(p); }
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
|
||||
const MAX_MRO_DEPTH = 5;
|
||||
|
||||
|
|
@ -592,6 +653,10 @@ export const buildTypeEnv = (
|
|||
const parentMap = options?.parentMap;
|
||||
const env: TypeEnv = new Map();
|
||||
const patternOverrides: PatternOverrides = new Map();
|
||||
// Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH
|
||||
// a base type annotation AND a more specific constructor initializer.
|
||||
// e.g., `Animal a = new Dog()` → constructorTypeMap.set('func@42\0a', 'Dog')
|
||||
const constructorTypeMap = new Map<string, string>();
|
||||
const localClassNames = new Set<string>();
|
||||
const classNames = createClassNameLookup(localClassNames, symbolTable);
|
||||
const config = typeConfigs[language];
|
||||
|
|
@ -762,6 +827,22 @@ export const buildTypeEnv = (
|
|||
if (config.extractInitializer) {
|
||||
config.extractInitializer(node, scopeEnv, classNames);
|
||||
}
|
||||
|
||||
// Phase P: detect constructor-visible virtual dispatch.
|
||||
// When a declaration has BOTH a type annotation AND a constructor initializer,
|
||||
// record the constructor type for receiver override at call resolution time.
|
||||
// e.g., `Animal a = new Dog()` → constructorTypeMap.set('scope\0a', 'Dog')
|
||||
if (keysBefore) {
|
||||
for (const varName of scopeEnv.keys()) {
|
||||
if (keysBefore.has(varName)) continue;
|
||||
const declaredType = scopeEnv.get(varName);
|
||||
if (!declaredType) continue;
|
||||
const ctorType = extractConstructorTypeName(node);
|
||||
if (ctorType && ctorType !== declaredType) {
|
||||
constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -914,6 +995,7 @@ export const buildTypeEnv = (
|
|||
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
|
||||
constructorBindings: bindings,
|
||||
env,
|
||||
constructorTypeMap,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue