mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-01 07:02:35 +00:00
Merge pull request #387 from abhigyanpatwari/feat/type-resolution-milestone-d
feat(type-resolution): Milestone D — Phases A, B, C + Kotlin fix + 11-language integration tests
This commit is contained in:
commit
f05e84a4aa
88 changed files with 2098 additions and 245 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2094 symbols, 4982 relationships, 159 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2169 symbols, 5213 relationships, 165 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **GitNexus** (2094 symbols, 4982 relationships, 159 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **GitNexus** (2169 symbols, 5213 relationships, 165 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,27 @@ export const processCalls = async (
|
|||
}
|
||||
|
||||
const lang = getLanguageFromFilename(file.path);
|
||||
const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
|
||||
|
||||
// Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
|
||||
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
||||
const fileParentMap = new Map<string, string[]>();
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach(c => captureMap[c.name] = c.node);
|
||||
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
||||
const className: string = captureMap['heritage.class'].text;
|
||||
const parentName: string = captureMap['heritage.extends'].text;
|
||||
const extendsNode = captureMap['heritage.extends'];
|
||||
const fieldDecl = extendsNode.parent;
|
||||
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name')) continue;
|
||||
let parents = fileParentMap.get(className);
|
||||
if (!parents) { parents = []; fileParentMap.set(className, parents); }
|
||||
if (!parents.includes(parentName)) parents.push(parentName);
|
||||
}
|
||||
}
|
||||
const parentMap: ReadonlyMap<string, readonly string[]> = fileParentMap;
|
||||
|
||||
const typeEnv = lang ? buildTypeEnv(tree, lang, { symbolTable: ctx.symbols, parentMap }) : null;
|
||||
const callRouter = callRouters[language];
|
||||
|
||||
const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
|
||||
|
|
|
|||
|
|
@ -350,6 +350,16 @@ export const GO_QUERIES = `
|
|||
operand: (_) @assignment.receiver
|
||||
field: (field_identifier) @assignment.property))
|
||||
right: (_)) @assignment
|
||||
|
||||
; Write access: obj.field++ / obj.field--
|
||||
(inc_statement
|
||||
(selector_expression
|
||||
operand: (_) @assignment.receiver
|
||||
field: (field_identifier) @assignment.property)) @assignment
|
||||
(dec_statement
|
||||
(selector_expression
|
||||
operand: (_) @assignment.receiver
|
||||
field: (field_identifier) @assignment.property)) @assignment
|
||||
`;
|
||||
|
||||
// C++ queries - works with tree-sitter-cpp
|
||||
|
|
|
|||
|
|
@ -61,17 +61,22 @@ interface PatternOverride {
|
|||
/** scope → varName → overrides (checked in order, first range match wins) */
|
||||
type PatternOverrides = Map<string, Map<string, PatternOverride[]>>;
|
||||
|
||||
/** AST node types that represent mutually exclusive branch containers for pattern bindings. */
|
||||
const PATTERN_BRANCH_TYPES = new Set([
|
||||
/** AST node types that represent mutually exclusive branch containers for pattern bindings.
|
||||
* Includes both multi-arm pattern-match branches AND if-statement bodies for null-check narrowing. */
|
||||
const NARROWING_BRANCH_TYPES = new Set([
|
||||
'when_entry', // Kotlin when
|
||||
'switch_block_label', // Java switch (enhanced)
|
||||
'if_statement', // TS/JS, Java, C/C++
|
||||
'if_expression', // Kotlin (if is an expression)
|
||||
'statement_block', // TS/JS: { ... } body of if
|
||||
'control_structure_body', // Kotlin: body of if
|
||||
]);
|
||||
|
||||
/** Walk up the AST from a pattern node to find the enclosing branch container. */
|
||||
const findPatternBranchScope = (node: SyntaxNode): SyntaxNode | undefined => {
|
||||
const findNarrowingBranchScope = (node: SyntaxNode): SyntaxNode | undefined => {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (PATTERN_BRANCH_TYPES.has(current.type)) return current;
|
||||
if (NARROWING_BRANCH_TYPES.has(current.type)) return current;
|
||||
if (FUNCTION_NODE_TYPES.has(current.type)) return undefined;
|
||||
current = current.parent;
|
||||
}
|
||||
|
|
@ -161,6 +166,23 @@ const findEnclosingClassName = (node: SyntaxNode): string | undefined => {
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/** Keywords that refer to the current instance across languages. */
|
||||
const THIS_RECEIVERS = new Set(['this', 'self', '$this', 'Me']);
|
||||
|
||||
/**
|
||||
* If a pending assignment's receiver is this/self/$this/Me, substitute the
|
||||
* enclosing class name. Returns the item unchanged for non-receiver kinds
|
||||
* or when the receiver is not a this-keyword. Properties are readonly in the
|
||||
* discriminated union, so a new object is returned when substitution occurs.
|
||||
*/
|
||||
const substituteThisReceiver = (item: PendingAssignment, node: SyntaxNode): PendingAssignment => {
|
||||
if (item.kind !== 'fieldAccess' && item.kind !== 'methodCallResult') return item;
|
||||
if (!THIS_RECEIVERS.has(item.receiver)) return item;
|
||||
const className = findEnclosingClassName(node);
|
||||
if (!className) return item;
|
||||
return { ...item, receiver: className };
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk up the AST to find the enclosing class, then extract its parent class name
|
||||
* from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
|
||||
|
|
@ -366,50 +388,208 @@ const SKIP_SUBTREE_TYPES = new Set([
|
|||
|
||||
const CLASS_LIKE_TYPES = new Set(['Class', 'Struct', 'Interface']);
|
||||
|
||||
/** Memoize class definition lookups during fixpoint iteration.
|
||||
* SymbolTable is immutable during type resolution, so results never change.
|
||||
* Eliminates redundant array allocations + filter scans across iterations. */
|
||||
const createClassDefCache = (symbolTable?: SymbolTable) => {
|
||||
const cache = new Map<string, Array<{ nodeId: string; type: string }>>();
|
||||
return (typeName: string) => {
|
||||
let result = cache.get(typeName);
|
||||
if (result === undefined) {
|
||||
result = symbolTable
|
||||
? symbolTable.lookupFuzzy(typeName).filter(d => CLASS_LIKE_TYPES.has(d.type))
|
||||
: [];
|
||||
cache.set(typeName, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
||||
/** 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. */
|
||||
const walkParentChain = <T>(
|
||||
typeName: string,
|
||||
parentMap: ReadonlyMap<string, readonly string[]> | undefined,
|
||||
getClassDefs: (name: string) => Array<{ nodeId: string; type: string }>,
|
||||
lookupOnClass: (nodeId: string) => T | undefined,
|
||||
): T | undefined => {
|
||||
if (!parentMap) return undefined;
|
||||
const visited = new Set<string>([typeName]);
|
||||
let current = [typeName];
|
||||
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 parent of parents) {
|
||||
if (visited.has(parent)) continue;
|
||||
visited.add(parent);
|
||||
const parentDefs = getClassDefs(parent);
|
||||
if (parentDefs.length === 1) {
|
||||
const result = lookupOnClass(parentDefs[0].nodeId);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
next.push(parent);
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Resolve a field's declared type given a receiver variable and field name.
|
||||
* Uses SymbolTable to find the class nodeId for the receiver's type, then
|
||||
* looks up the field via the eagerly-populated fieldByOwner index. */
|
||||
* looks up the field via the eagerly-populated fieldByOwner index.
|
||||
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
||||
const resolveFieldType = (
|
||||
receiver: string, field: string,
|
||||
scopeEnv: ReadonlyMap<string, string>, symbolTable?: SymbolTable,
|
||||
getClassDefs?: (typeName: string) => Array<{ nodeId: string; type: string }>,
|
||||
parentMap?: ReadonlyMap<string, readonly string[]>,
|
||||
): string | undefined => {
|
||||
if (!symbolTable) return undefined;
|
||||
const receiverType = scopeEnv.get(receiver);
|
||||
if (!receiverType) return undefined;
|
||||
const classDefs = symbolTable.lookupFuzzy(receiverType)
|
||||
.filter(d => CLASS_LIKE_TYPES.has(d.type));
|
||||
const lookup = getClassDefs
|
||||
?? ((name: string) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
||||
const classDefs = lookup(receiverType);
|
||||
if (classDefs.length !== 1) return undefined;
|
||||
// Direct lookup first
|
||||
const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
|
||||
if (!fieldDef?.declaredType) return undefined;
|
||||
return extractReturnTypeName(fieldDef.declaredType);
|
||||
if (fieldDef?.declaredType) return extractReturnTypeName(fieldDef.declaredType);
|
||||
// MRO parent chain walking on miss
|
||||
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
||||
const f = symbolTable.lookupFieldByOwner(nodeId, field);
|
||||
return f?.declaredType ? extractReturnTypeName(f.declaredType) : undefined;
|
||||
});
|
||||
return inherited;
|
||||
};
|
||||
|
||||
/** Resolve a method's return type given a receiver variable and method name.
|
||||
* Uses SymbolTable to find class nodeIds for the receiver's type, then
|
||||
* looks up the method via lookupFuzzyCallable filtered by ownerId. */
|
||||
* looks up the method via lookupFuzzyCallable filtered by ownerId.
|
||||
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
||||
const resolveMethodReturnType = (
|
||||
receiver: string, method: string,
|
||||
scopeEnv: ReadonlyMap<string, string>, symbolTable?: SymbolTable,
|
||||
getClassDefs?: (typeName: string) => Array<{ nodeId: string; type: string }>,
|
||||
parentMap?: ReadonlyMap<string, readonly string[]>,
|
||||
): string | undefined => {
|
||||
if (!symbolTable) return undefined;
|
||||
const receiverType = scopeEnv.get(receiver);
|
||||
if (!receiverType) return undefined;
|
||||
const classDefs = symbolTable.lookupFuzzy(receiverType)
|
||||
.filter(d => CLASS_LIKE_TYPES.has(d.type));
|
||||
const lookup = getClassDefs
|
||||
?? ((name: string) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
||||
const classDefs = lookup(receiverType);
|
||||
if (classDefs.length === 0) return undefined;
|
||||
// Direct lookup first
|
||||
const classNodeIds = new Set(classDefs.map(d => d.nodeId));
|
||||
const methods = symbolTable.lookupFuzzyCallable(method)
|
||||
.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
||||
if (methods.length !== 1) return undefined;
|
||||
if (!methods[0].returnType) return undefined;
|
||||
return extractReturnTypeName(methods[0].returnType);
|
||||
if (methods.length === 1 && methods[0].returnType) {
|
||||
return extractReturnTypeName(methods[0].returnType);
|
||||
}
|
||||
// MRO parent chain walking on miss
|
||||
if (methods.length === 0) {
|
||||
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
||||
const parentMethods = symbolTable.lookupFuzzyCallable(method)
|
||||
.filter(d => d.ownerId === nodeId);
|
||||
if (parentMethods.length !== 1 || !parentMethods[0].returnType) return undefined;
|
||||
return extractReturnTypeName(parentMethods[0].returnType);
|
||||
});
|
||||
return inherited;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
|
||||
* fieldAccess, methodCallResult) until no new bindings are produced.
|
||||
* Handles arbitrary-depth mixed chains:
|
||||
* const user = getUser(); // callResult → User
|
||||
* const addr = user.address; // fieldAccess → Address (depends on user)
|
||||
* const city = addr.getCity(); // methodCallResult → City (depends on addr)
|
||||
* const alias = city; // copy → City (depends on city)
|
||||
* Data flow: SymbolTable (immutable) + scopeEnv → resolve → scopeEnv.
|
||||
* Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
|
||||
*/
|
||||
const MAX_FIXPOINT_ITERATIONS = 10;
|
||||
|
||||
const resolveFixpointBindings = (
|
||||
pendingItems: Array<{ scope: string } & PendingAssignment>,
|
||||
env: TypeEnv,
|
||||
returnTypeLookup: ReturnTypeLookup,
|
||||
symbolTable?: SymbolTable,
|
||||
parentMap?: ReadonlyMap<string, readonly string[]>,
|
||||
): void => {
|
||||
if (pendingItems.length === 0) return;
|
||||
const getClassDefs = createClassDefCache(symbolTable);
|
||||
const resolved = new Set<number>();
|
||||
for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
|
||||
let changed = false;
|
||||
for (let i = 0; i < pendingItems.length; i++) {
|
||||
if (resolved.has(i)) continue;
|
||||
const item = pendingItems[i];
|
||||
const scopeEnv = env.get(item.scope);
|
||||
if (!scopeEnv || scopeEnv.has(item.lhs)) { resolved.add(i); continue; }
|
||||
|
||||
let typeName: string | undefined;
|
||||
switch (item.kind) {
|
||||
case 'callResult':
|
||||
typeName = returnTypeLookup.lookupReturnType(item.callee);
|
||||
break;
|
||||
case 'copy':
|
||||
typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
|
||||
break;
|
||||
case 'fieldAccess':
|
||||
typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable, getClassDefs, parentMap);
|
||||
break;
|
||||
case 'methodCallResult':
|
||||
typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable, getClassDefs, parentMap);
|
||||
break;
|
||||
default: {
|
||||
// Exhaustive check: TypeScript will error here if a new PendingAssignment
|
||||
// kind is added without handling it in the switch.
|
||||
const _exhaustive: never = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (typeName) {
|
||||
scopeEnv.set(item.lhs, typeName);
|
||||
resolved.add(i);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) break;
|
||||
if (iter === MAX_FIXPOINT_ITERATIONS - 1 && process.env.GITNEXUS_DEBUG) {
|
||||
const unresolved = pendingItems.length - resolved.size;
|
||||
if (unresolved > 0) {
|
||||
console.warn(`[type-env] fixpoint hit iteration cap (${MAX_FIXPOINT_ITERATIONS}), ${unresolved} items unresolved`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for buildTypeEnv.
|
||||
* Uses an options object to allow future extensions without positional parameter sprawl.
|
||||
*/
|
||||
export interface BuildTypeEnvOptions {
|
||||
symbolTable?: SymbolTable;
|
||||
parentMap?: ReadonlyMap<string, readonly string[]>;
|
||||
}
|
||||
|
||||
export const buildTypeEnv = (
|
||||
tree: { rootNode: SyntaxNode },
|
||||
language: SupportedLanguages,
|
||||
symbolTable?: SymbolTable,
|
||||
options?: BuildTypeEnvOptions,
|
||||
): TypeEnvironment => {
|
||||
const symbolTable = options?.symbolTable;
|
||||
const parentMap = options?.parentMap;
|
||||
const env: TypeEnv = new Map();
|
||||
const patternOverrides: PatternOverrides = new Map();
|
||||
const localClassNames = new Set<string>();
|
||||
|
|
@ -448,6 +628,9 @@ export const buildTypeEnv = (
|
|||
// methodCallResult items during walk(), then iterates until no new bindings are produced.
|
||||
// Handles arbitrary-depth mixed chains: callResult → fieldAccess → methodCallResult → copy.
|
||||
const pendingItems: Array<{ scope: string } & PendingAssignment> = [];
|
||||
// For-loop nodes whose iterable was unresolved at walk-time. Replayed after the fixpoint
|
||||
// resolves the iterable's type, bridging the walk-time/fixpoint gap (Phase 10 / ex-9B).
|
||||
const pendingForLoops: Array<{ node: SyntaxNode; scope: string }> = [];
|
||||
// Maps `scope\0varName` → the type annotation AST node from the original declaration.
|
||||
// Allows pattern extractors to navigate back to the declaration's generic type arguments
|
||||
// (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
|
||||
|
|
@ -498,7 +681,8 @@ export const buildTypeEnv = (
|
|||
fallbackName = child;
|
||||
}
|
||||
if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
|
||||
|| child.type === 'generic_type' || child.type === 'parameterized_type')) {
|
||||
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
||||
|| child.type === 'nullable_type')) {
|
||||
fallbackType = child;
|
||||
}
|
||||
}
|
||||
|
|
@ -516,8 +700,14 @@ export const buildTypeEnv = (
|
|||
// Checked before declarationNodeTypes — loop variables are not declarations.
|
||||
if (config.forLoopNodeTypes?.has(node.type)) {
|
||||
if (config.extractForLoopBinding) {
|
||||
const sizeBefore = scopeEnv.size;
|
||||
const forLoopCtx: ForLoopExtractorContext = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
|
||||
config.extractForLoopBinding(node, forLoopCtx);
|
||||
// If no new binding was produced, the iterable's type may not yet be resolved.
|
||||
// Store for post-fixpoint replay (Phase 10 / ex-9B loop-fixpoint bridge).
|
||||
if (scopeEnv.size === sizeBefore) {
|
||||
pendingForLoops.push({ node, scope });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -605,7 +795,8 @@ export const buildTypeEnv = (
|
|||
}
|
||||
|
||||
// Pattern binding extraction: handles constructs that introduce NEW typed variables
|
||||
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`).
|
||||
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
|
||||
// or narrow existing variables within a branch (null-check narrowing).
|
||||
// Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
|
||||
// Conservative: extractor returns undefined when source type is unknown.
|
||||
if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
|
||||
|
|
@ -614,11 +805,22 @@ export const buildTypeEnv = (
|
|||
const scopeEnv = env.get(scope)!;
|
||||
const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
|
||||
if (patternBinding) {
|
||||
if (config.allowPatternBindingOverwrite) {
|
||||
if (patternBinding.narrowingRange) {
|
||||
// Explicit narrowing range (null-check narrowing): always store in patternOverrides
|
||||
// using the extractor-provided range (typically the if-body block).
|
||||
if (!patternOverrides.has(scope)) patternOverrides.set(scope, new Map());
|
||||
const varMap = patternOverrides.get(scope)!;
|
||||
if (!varMap.has(patternBinding.varName)) varMap.set(patternBinding.varName, []);
|
||||
varMap.get(patternBinding.varName)!.push({
|
||||
rangeStart: patternBinding.narrowingRange.startIndex,
|
||||
rangeEnd: patternBinding.narrowingRange.endIndex,
|
||||
typeName: patternBinding.typeName,
|
||||
});
|
||||
} else if (config.allowPatternBindingOverwrite) {
|
||||
// Position-indexed: store per-branch binding for smart-cast narrowing.
|
||||
// Each when arm / switch case gets its own type for the variable,
|
||||
// preventing cross-arm contamination (e.g., Kotlin when/is).
|
||||
const branchNode = findPatternBranchScope(node);
|
||||
const branchNode = findNarrowingBranchScope(node);
|
||||
if (branchNode) {
|
||||
if (!patternOverrides.has(scope)) patternOverrides.set(scope, new Map());
|
||||
const varMap = patternOverrides.get(scope)!;
|
||||
|
|
@ -643,6 +845,7 @@ export const buildTypeEnv = (
|
|||
// Delegates to per-language extractPendingAssignment — AST shapes differ widely
|
||||
// (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
|
||||
// Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
|
||||
// May return a single item or an array (for destructuring: N fieldAccess items).
|
||||
if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
|
||||
// scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
|
||||
// of interestingNodeTypes, so extractTypeBinding already created the scope map above.
|
||||
|
|
@ -650,7 +853,12 @@ export const buildTypeEnv = (
|
|||
if (scopeEnv) {
|
||||
const pending = config.extractPendingAssignment(node, scopeEnv);
|
||||
if (pending) {
|
||||
pendingItems.push({ scope, ...pending });
|
||||
const items = Array.isArray(pending) ? pending : [pending];
|
||||
for (const item of items) {
|
||||
// Substitute this/self/$this/Me receivers with enclosing class name
|
||||
const resolved = substituteThisReceiver(item, node);
|
||||
pendingItems.push({ scope, ...resolved });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -676,47 +884,30 @@ export const buildTypeEnv = (
|
|||
|
||||
walk(tree.rootNode, FILE_SCOPE);
|
||||
|
||||
// Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
|
||||
// fieldAccess, methodCallResult) until no new bindings are produced.
|
||||
// Handles arbitrary-depth mixed chains:
|
||||
// const user = getUser(); // callResult → User
|
||||
// const addr = user.address; // fieldAccess → Address (depends on user)
|
||||
// const city = addr.getCity(); // methodCallResult → City (depends on addr)
|
||||
// const alias = city; // copy → City (depends on city)
|
||||
// Data flow: SymbolTable (immutable) + scopeEnv → resolve → scopeEnv.
|
||||
// Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
|
||||
const MAX_FIXPOINT_ITERATIONS = 10;
|
||||
const resolved = new Set<number>();
|
||||
for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
|
||||
let changed = false;
|
||||
for (let i = 0; i < pendingItems.length; i++) {
|
||||
if (resolved.has(i)) continue;
|
||||
const item = pendingItems[i];
|
||||
const scopeEnv = env.get(item.scope);
|
||||
if (!scopeEnv || scopeEnv.has(item.lhs)) { resolved.add(i); continue; }
|
||||
resolveFixpointBindings(pendingItems, env, returnTypeLookup, symbolTable, parentMap);
|
||||
|
||||
let typeName: string | undefined;
|
||||
switch (item.kind) {
|
||||
case 'callResult':
|
||||
typeName = returnTypeLookup.lookupReturnType(item.callee);
|
||||
break;
|
||||
case 'copy':
|
||||
typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
|
||||
break;
|
||||
case 'fieldAccess':
|
||||
typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable);
|
||||
break;
|
||||
case 'methodCallResult':
|
||||
typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable);
|
||||
break;
|
||||
}
|
||||
if (typeName) {
|
||||
scopeEnv.set(item.lhs, typeName);
|
||||
resolved.add(i);
|
||||
changed = true;
|
||||
}
|
||||
// Post-fixpoint for-loop replay (Phase 10 / ex-9B loop-fixpoint bridge):
|
||||
// For-loop nodes whose iterables were unresolved at walk-time may now be
|
||||
// resolvable because the fixpoint bound the iterable's type.
|
||||
// Example: `const users = getUsers(); for (const u of users) { u.save(); }`
|
||||
// - walk-time: users untyped → u unresolved
|
||||
// - fixpoint: users → User[]
|
||||
// - replay: users now typed → u → User
|
||||
if (pendingForLoops.length > 0 && config.extractForLoopBinding) {
|
||||
for (const { node, scope } of pendingForLoops) {
|
||||
if (!env.has(scope)) env.set(scope, new Map());
|
||||
const scopeEnv = env.get(scope)!;
|
||||
config.extractForLoopBinding(node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup });
|
||||
}
|
||||
// Re-run the main fixpoint to resolve items that depended on loop variables.
|
||||
// Only needed if replay actually produced new bindings.
|
||||
const unresolvedBefore = pendingItems.filter((item) => {
|
||||
const scopeEnv = env.get(item.scope);
|
||||
return scopeEnv && !scopeEnv.has(item.lhs);
|
||||
});
|
||||
if (unresolvedBefore.length > 0) {
|
||||
resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
|
||||
}
|
||||
if (!changed) break;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -278,18 +278,82 @@ const extractForLoopBinding: ForLoopExtractor = (node, { scopeEnv, declarationTy
|
|||
* declaration_pattern, or when the type/name cannot be extracted.
|
||||
* No scopeEnv lookup is needed — the pattern explicitly declares the new variable's type.
|
||||
*/
|
||||
const extractPatternBinding: PatternBindingExtractor = (node) => {
|
||||
/**
|
||||
* Find the if-body (consequence) block for a C# null-check.
|
||||
* Walks up from the expression to find the enclosing if_statement,
|
||||
* then returns its first block child (the truthy branch body).
|
||||
*/
|
||||
const findCSharpIfConsequenceBlock = (expr: SyntaxNode): SyntaxNode | undefined => {
|
||||
let current = expr.parent;
|
||||
while (current) {
|
||||
if (current.type === 'if_statement') {
|
||||
// C# if_statement consequence is the 'consequence' field or first block child
|
||||
const consequence = current.childForFieldName('consequence');
|
||||
if (consequence) return consequence;
|
||||
for (let i = 0; i < current.childCount; i++) {
|
||||
const child = current.child(i);
|
||||
if (child?.type === 'block') return child;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (current.type === 'block' || current.type === 'method_declaration'
|
||||
|| current.type === 'constructor_declaration' || current.type === 'local_function_statement'
|
||||
|| current.type === 'lambda_expression') return undefined;
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Check if a C# declaration type node represents a nullable type.
|
||||
* Checks for nullable_type AST node or '?' in the type text (e.g., User?). */
|
||||
const isCSharpNullableDecl = (declTypeNode: SyntaxNode): boolean => {
|
||||
if (declTypeNode.type === 'nullable_type') return true;
|
||||
return declTypeNode.text.includes('?');
|
||||
};
|
||||
|
||||
const extractPatternBinding: PatternBindingExtractor = (node, scopeEnv, declarationTypeNodes, scope) => {
|
||||
// is_pattern_expression: `obj is User user` — has a declaration_pattern child
|
||||
// Also handles `x is not null` for null-check narrowing
|
||||
if (node.type === 'is_pattern_expression') {
|
||||
const pattern = node.childForFieldName('pattern');
|
||||
if (pattern?.type !== 'declaration_pattern' && pattern?.type !== 'recursive_pattern') return undefined;
|
||||
const typeNode = pattern.childForFieldName('type');
|
||||
const nameNode = pattern.childForFieldName('name');
|
||||
if (!typeNode || !nameNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!typeName || !varName) return undefined;
|
||||
return { varName, typeName };
|
||||
if (!pattern) return undefined;
|
||||
|
||||
// Standard type pattern: `obj is User user`
|
||||
if (pattern.type === 'declaration_pattern' || pattern.type === 'recursive_pattern') {
|
||||
const typeNode = pattern.childForFieldName('type');
|
||||
const nameNode = pattern.childForFieldName('name');
|
||||
if (!typeNode || !nameNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!typeName || !varName) return undefined;
|
||||
return { varName, typeName };
|
||||
}
|
||||
|
||||
// Null-check: `x is not null` — negated_pattern > constant_pattern > null_literal
|
||||
if (pattern.type === 'negated_pattern') {
|
||||
const inner = pattern.firstNamedChild;
|
||||
if (inner?.type === 'constant_pattern') {
|
||||
const literal = inner.firstNamedChild ?? inner.firstChild;
|
||||
if (literal?.type === 'null_literal' || literal?.text === 'null') {
|
||||
const expr = node.childForFieldName('expression');
|
||||
if (!expr || expr.type !== 'identifier') return undefined;
|
||||
const varName = expr.text;
|
||||
const resolvedType = scopeEnv.get(varName);
|
||||
if (!resolvedType) return undefined;
|
||||
// Verify the original declaration was nullable
|
||||
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
||||
if (!declTypeNode || !isCSharpNullableDecl(declTypeNode)) return undefined;
|
||||
const ifBody = findCSharpIfConsequenceBlock(node);
|
||||
if (!ifBody) return undefined;
|
||||
return {
|
||||
varName,
|
||||
typeName: resolvedType,
|
||||
narrowingRange: { startIndex: ifBody.startIndex, endIndex: ifBody.endIndex },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
// declaration_pattern / recursive_pattern: standalone in switch statements and switch expressions
|
||||
// `case User u:` or `User u =>` or `User { Name: "Alice" } u =>`
|
||||
|
|
@ -303,6 +367,34 @@ const extractPatternBinding: PatternBindingExtractor = (node) => {
|
|||
if (!typeName || !varName) return undefined;
|
||||
return { varName, typeName };
|
||||
}
|
||||
// Null-check: `x != null` — binary_expression with != operator
|
||||
if (node.type === 'binary_expression') {
|
||||
const op = node.children.find(c => !c.isNamed && c.text === '!=');
|
||||
if (!op) return undefined;
|
||||
const left = node.namedChild(0);
|
||||
const right = node.namedChild(1);
|
||||
if (!left || !right) return undefined;
|
||||
let varNode: SyntaxNode | undefined;
|
||||
if (left.type === 'identifier' && (right.type === 'null_literal' || right.text === 'null')) {
|
||||
varNode = left;
|
||||
} else if (right.type === 'identifier' && (left.type === 'null_literal' || left.text === 'null')) {
|
||||
varNode = right;
|
||||
}
|
||||
if (!varNode) return undefined;
|
||||
const varName = varNode.text;
|
||||
const resolvedType = scopeEnv.get(varName);
|
||||
if (!resolvedType) return undefined;
|
||||
// Verify the original declaration was nullable
|
||||
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
||||
if (!declTypeNode || !isCSharpNullableDecl(declTypeNode)) return undefined;
|
||||
const ifBody = findCSharpIfConsequenceBlock(node);
|
||||
if (!ifBody) return undefined;
|
||||
return {
|
||||
varName,
|
||||
typeName: resolvedType,
|
||||
narrowingRange: { startIndex: ifBody.startIndex, endIndex: ifBody.endIndex },
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
|
@ -374,7 +466,7 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['is_pattern_expression', 'declaration_pattern', 'recursive_pattern']),
|
||||
patternBindingNodeTypes: new Set(['is_pattern_expression', 'declaration_pattern', 'recursive_pattern', 'binary_expression']),
|
||||
extractDeclaration,
|
||||
extractParameter,
|
||||
scanConstructorBinding,
|
||||
|
|
|
|||
|
|
@ -295,7 +295,8 @@ const extractKotlinDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: M
|
|||
const varDecl = findChildByType(node, 'variable_declaration');
|
||||
if (varDecl) {
|
||||
const nameNode = findChildByType(varDecl, 'simple_identifier');
|
||||
const typeNode = findChildByType(varDecl, 'user_type');
|
||||
const typeNode = findChildByType(varDecl, 'user_type')
|
||||
?? findChildByType(varDecl, 'nullable_type');
|
||||
if (!nameNode || !typeNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
|
|
@ -341,7 +342,8 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<s
|
|||
|
||||
// Fallback: Kotlin `parameter` nodes use positional children, not named fields
|
||||
if (!nameNode) nameNode = findChildByType(node, 'simple_identifier');
|
||||
if (!typeNode) typeNode = findChildByType(node, 'user_type');
|
||||
if (!typeNode) typeNode = findChildByType(node, 'user_type')
|
||||
?? findChildByType(node, 'nullable_type');
|
||||
|
||||
if (!nameNode || !typeNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
|
|
@ -660,27 +662,76 @@ const findAncestorByType = (node: SyntaxNode, type: string): SyntaxNode | undefi
|
|||
return undefined;
|
||||
};
|
||||
|
||||
const extractKotlinPatternBinding: PatternBindingExtractor = (node) => {
|
||||
if (node.type !== 'type_test') return undefined;
|
||||
const typeNode = node.lastNamedChild;
|
||||
if (!typeNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (!typeName) return undefined;
|
||||
const whenExpr = findAncestorByType(node, 'when_expression');
|
||||
if (!whenExpr) return undefined;
|
||||
const whenSubject = whenExpr.namedChild(0);
|
||||
const subject = whenSubject?.firstNamedChild ?? whenSubject;
|
||||
if (!subject) return undefined;
|
||||
const varName = extractVarName(subject);
|
||||
if (!varName) return undefined;
|
||||
return { varName, typeName };
|
||||
const extractKotlinPatternBinding: PatternBindingExtractor = (node, scopeEnv, declarationTypeNodes, scope) => {
|
||||
// Kotlin when/is smart casts (existing behavior)
|
||||
if (node.type === 'type_test') {
|
||||
const typeNode = node.lastNamedChild;
|
||||
if (!typeNode) return undefined;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (!typeName) return undefined;
|
||||
const whenExpr = findAncestorByType(node, 'when_expression');
|
||||
if (!whenExpr) return undefined;
|
||||
const whenSubject = whenExpr.namedChild(0);
|
||||
const subject = whenSubject?.firstNamedChild ?? whenSubject;
|
||||
if (!subject) return undefined;
|
||||
const varName = extractVarName(subject);
|
||||
if (!varName) return undefined;
|
||||
return { varName, typeName };
|
||||
}
|
||||
|
||||
// Null-check narrowing: if (x != null) { ... }
|
||||
// Kotlin AST: equality_expression > simple_identifier, "!=" [anon], "null" [anon]
|
||||
// Note: `null` is an anonymous node in tree-sitter-kotlin, not `null_literal`.
|
||||
if (node.type === 'equality_expression') {
|
||||
const op = node.children.find(c => !c.isNamed && c.text === '!=');
|
||||
if (!op) return undefined;
|
||||
|
||||
// `null` is anonymous in Kotlin grammar — use positional child scan
|
||||
let varNode: SyntaxNode | undefined;
|
||||
let hasNull = false;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (!c) continue;
|
||||
if (c.type === 'simple_identifier') varNode = c;
|
||||
if (!c.isNamed && c.text === 'null') hasNull = true;
|
||||
}
|
||||
if (!varNode || !hasNull) return undefined;
|
||||
|
||||
const varName = varNode.text;
|
||||
const resolvedType = scopeEnv.get(varName);
|
||||
if (!resolvedType) return undefined;
|
||||
|
||||
// Check if the original declaration type was nullable (ends with ?)
|
||||
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
||||
if (!declTypeNode) return undefined;
|
||||
const declText = declTypeNode.text;
|
||||
if (!declText.includes('?') && !declText.includes('null')) return undefined;
|
||||
|
||||
// Find the if-body: walk up to if_expression, then find control_structure_body
|
||||
const ifExpr = findAncestorByType(node, 'if_expression');
|
||||
if (!ifExpr) return undefined;
|
||||
// The consequence is the first control_structure_body child
|
||||
for (let i = 0; i < ifExpr.childCount; i++) {
|
||||
const child = ifExpr.child(i);
|
||||
if (child?.type === 'control_structure_body') {
|
||||
return {
|
||||
varName,
|
||||
typeName: resolvedType,
|
||||
narrowingRange: { startIndex: child.startIndex, endIndex: child.endIndex },
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const kotlinTypeConfig: LanguageTypeConfig = {
|
||||
allowPatternBindingOverwrite: true,
|
||||
declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: KOTLIN_FOR_LOOP_NODE_TYPES,
|
||||
patternBindingNodeTypes: new Set(['type_test']),
|
||||
patternBindingNodeTypes: new Set(['type_test', 'equality_expression']),
|
||||
extractDeclaration: extractKotlinDeclaration,
|
||||
extractParameter: extractKotlinParameter,
|
||||
extractInitializer: extractKotlinInitializer,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PatternBindingExtractor, ForLoopExtractor } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, PendingAssignmentExtractor, PendingAssignment, PatternBindingExtractor, ForLoopExtractor } from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString, type TypeArgPosition } from './shared.js';
|
||||
|
||||
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
|
|
@ -189,12 +189,45 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
|||
return { varName: patternNode.text, calleeName };
|
||||
};
|
||||
|
||||
/** Rust: let alias = u; → let_declaration with pattern + value fields */
|
||||
/** Rust: let alias = u; → let_declaration with pattern + value fields.
|
||||
* Also handles struct destructuring: `let Point { x, y } = p` → N fieldAccess items. */
|
||||
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
if (node.type !== 'let_declaration') return undefined;
|
||||
const pattern = node.childForFieldName('pattern');
|
||||
const value = node.childForFieldName('value');
|
||||
if (!pattern || !value) return undefined;
|
||||
|
||||
// Struct pattern destructuring: `let Point { x, y } = receiver`
|
||||
// struct_pattern has a type child (struct name) and field_pattern children
|
||||
if (pattern.type === 'struct_pattern' && value.type === 'identifier') {
|
||||
const receiver = value.text;
|
||||
const items: PendingAssignment[] = [];
|
||||
for (let j = 0; j < pattern.namedChildCount; j++) {
|
||||
const field = pattern.namedChild(j);
|
||||
if (!field) continue;
|
||||
if (field.type === 'field_pattern') {
|
||||
// `Point { x: local_x }` → field_pattern with name + pattern children
|
||||
const nameNode = field.childForFieldName('name');
|
||||
const patNode = field.childForFieldName('pattern');
|
||||
if (nameNode && patNode) {
|
||||
const fieldName = nameNode.text;
|
||||
const varName = extractVarName(patNode);
|
||||
if (varName && !scopeEnv.has(varName)) {
|
||||
items.push({ kind: 'fieldAccess', lhs: varName, receiver, field: fieldName });
|
||||
}
|
||||
} else if (nameNode) {
|
||||
// Shorthand: `Point { x }` → field_pattern with only name (varName = fieldName)
|
||||
const varName = nameNode.text;
|
||||
if (!scopeEnv.has(varName)) {
|
||||
items.push({ kind: 'fieldAccess', lhs: varName, receiver, field: varName });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.length > 0) return items;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lhs = extractVarName(pattern);
|
||||
if (!lhs || scopeEnv.has(lhs)) return undefined;
|
||||
// Unwrap Rust .await: `let user = get_user().await` → call_expression
|
||||
|
|
|
|||
|
|
@ -67,15 +67,29 @@ export type PendingAssignment =
|
|||
* Returns a PendingAssignment when the RHS is a bare identifier (`copy`), a
|
||||
* call expression (`callResult`), a field access (`fieldAccess`), or a
|
||||
* method call with receiver (`methodCallResult`) and the LHS has no resolved type yet.
|
||||
* May return an array of PendingAssignment items for destructuring patterns
|
||||
* (e.g., `const { a, b } = obj` emits N fieldAccess items).
|
||||
* Returns undefined if the node is not a matching assignment. */
|
||||
export type PendingAssignmentExtractor = (
|
||||
node: SyntaxNode,
|
||||
scopeEnv: ReadonlyMap<string, string>,
|
||||
) => PendingAssignment | undefined;
|
||||
) => PendingAssignment | PendingAssignment[] | undefined;
|
||||
|
||||
/** Result of a pattern binding extraction. */
|
||||
export interface PatternBindingResult {
|
||||
varName: string;
|
||||
typeName: string;
|
||||
/** Optional: AST node whose position range should be used for the patternOverride.
|
||||
* When present, the override uses this node's range instead of the auto-detected
|
||||
* branch scope. Used by null-check narrowing to target the if-body specifically. */
|
||||
narrowingRange?: { startIndex: number; endIndex: number };
|
||||
}
|
||||
|
||||
/** Extracts a typed variable binding from a pattern-matching construct.
|
||||
* Returns { varName, typeName } for patterns that introduce NEW variables.
|
||||
* Examples: `if let Some(user) = opt` (Rust), `x instanceof User user` (Java).
|
||||
* Returns { varName, typeName } for patterns that introduce NEW variables
|
||||
* or narrow existing variables (null-check narrowing).
|
||||
* Examples: `if let Some(user) = opt` (Rust), `x instanceof User user` (Java),
|
||||
* `if (x != null)` (null-check narrowing in TS/Kotlin/C#).
|
||||
* Conservative: returns undefined when the source variable's type is unknown.
|
||||
*
|
||||
* @param scopeEnv Read-only view of already-resolved type bindings in the current scope.
|
||||
|
|
@ -88,7 +102,7 @@ export type PatternBindingExtractor = (
|
|||
scopeEnv: ReadonlyMap<string, string>,
|
||||
declarationTypeNodes: ReadonlyMap<string, SyntaxNode>,
|
||||
scope: string,
|
||||
) => { varName: string; typeName: string } | undefined;
|
||||
) => PatternBindingResult | undefined;
|
||||
|
||||
/** Per-language type extraction configuration */
|
||||
export interface LanguageTypeConfig {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SyntaxNode } from '../utils.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, ForLoopExtractor, PatternBindingExtractor } from './types.js';
|
||||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup, ConstructorBindingScanner, ReturnTypeExtractor, PendingAssignmentExtractor, PendingAssignment, ForLoopExtractor, PatternBindingExtractor } 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([
|
||||
|
|
@ -429,7 +429,8 @@ const extractForLoopBinding: ForLoopExtractor = (node, { scopeEnv, declarationTy
|
|||
if (loopVarName) scopeEnv.set(loopVarName, elementType);
|
||||
};
|
||||
|
||||
/** TS/JS: const alias = u → variable_declarator with name/value fields */
|
||||
/** TS/JS: const alias = u → variable_declarator with name/value fields.
|
||||
* Also handles destructuring: `const { a, b } = obj` → N fieldAccess items. */
|
||||
const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
|
|
@ -437,6 +438,38 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
const nameNode = child.childForFieldName('name');
|
||||
const valueNode = child.childForFieldName('value');
|
||||
if (!nameNode || !valueNode) continue;
|
||||
|
||||
// Object destructuring: `const { address, name } = user`
|
||||
// Emits N fieldAccess items — one per destructured binding.
|
||||
if (nameNode.type === 'object_pattern' && valueNode.type === 'identifier') {
|
||||
const receiver = valueNode.text;
|
||||
const items: PendingAssignment[] = [];
|
||||
for (let j = 0; j < nameNode.namedChildCount; j++) {
|
||||
const prop = nameNode.namedChild(j);
|
||||
if (!prop) continue;
|
||||
if (prop.type === 'shorthand_property_identifier_pattern') {
|
||||
// `const { name } = user` → shorthand: varName = fieldName
|
||||
const varName = prop.text;
|
||||
if (!scopeEnv.has(varName)) {
|
||||
items.push({ kind: 'fieldAccess', lhs: varName, receiver, field: varName });
|
||||
}
|
||||
} else if (prop.type === 'pair_pattern') {
|
||||
// `const { address: addr } = user` → pair_pattern: key=field, value=varName
|
||||
const keyNode = prop.childForFieldName('key');
|
||||
const valNode = prop.childForFieldName('value');
|
||||
if (keyNode && valNode) {
|
||||
const fieldName = keyNode.text;
|
||||
const varName = valNode.text;
|
||||
if (!scopeEnv.has(varName)) {
|
||||
items.push({ kind: 'fieldAccess', lhs: varName, receiver, field: fieldName });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.length > 0) return items;
|
||||
continue;
|
||||
}
|
||||
|
||||
const lhs = nameNode.text;
|
||||
if (scopeEnv.has(lhs)) continue;
|
||||
if (valueNode.type === 'identifier') return { kind: 'copy', lhs, rhs: valueNode.text };
|
||||
|
|
@ -472,19 +505,96 @@ const extractPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) =>
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/** Null-check keywords that indicate a null-comparison in binary expressions. */
|
||||
const NULL_CHECK_KEYWORDS = new Set(['null', 'undefined']);
|
||||
|
||||
/**
|
||||
* Find the if-body (consequence) block for a null-check binary_expression.
|
||||
* Walks up from the binary_expression through parenthesized_expression to if_statement,
|
||||
* then returns the consequence block (statement_block).
|
||||
*
|
||||
* AST structure: if_statement > parenthesized_expression > binary_expression
|
||||
* if_statement > statement_block (consequence)
|
||||
*/
|
||||
const findIfConsequenceBlock = (binaryExpr: SyntaxNode): SyntaxNode | undefined => {
|
||||
// Walk up to find the if_statement (typically: binary_expression > parenthesized_expression > if_statement)
|
||||
let current = binaryExpr.parent;
|
||||
while (current) {
|
||||
if (current.type === 'if_statement') {
|
||||
// The consequence is the first statement_block child of if_statement
|
||||
for (let i = 0; i < current.childCount; i++) {
|
||||
const child = current.child(i);
|
||||
if (child?.type === 'statement_block') return child;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
// Stop climbing at function/block boundaries — don't cross scope
|
||||
if (current.type === 'function_declaration' || current.type === 'function_expression'
|
||||
|| current.type === 'arrow_function' || current.type === 'method_definition') return undefined;
|
||||
current = current.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** TS instanceof narrowing: `x instanceof User` → bind x to User.
|
||||
* Only works when x has no prior type binding (e.g. x: unknown, untyped params).
|
||||
* Typed params (x: Animal) are blocked by the !scopeEnv.has() guard in buildTypeEnv.
|
||||
* Uses first-writer-wins, same as Rust match arm bindings. */
|
||||
const extractPatternBinding: PatternBindingExtractor = (node) => {
|
||||
* Also handles null-check narrowing: `x !== null`, `x != undefined` etc.
|
||||
* instanceof: first-writer-wins (no prior type binding).
|
||||
* null-check: position-indexed narrowing via narrowingRange. */
|
||||
const extractPatternBinding: PatternBindingExtractor = (node, scopeEnv, declarationTypeNodes, scope) => {
|
||||
if (node.type !== 'binary_expression') return undefined;
|
||||
const op = node.children.find(c => !c.isNamed && c.text === 'instanceof');
|
||||
|
||||
// Check for instanceof first (existing behavior)
|
||||
const instanceofOp = node.children.find(c => !c.isNamed && c.text === 'instanceof');
|
||||
if (instanceofOp) {
|
||||
const left = node.namedChild(0);
|
||||
const right = node.namedChild(1);
|
||||
if (left?.type !== 'identifier' || right?.type !== 'identifier') return undefined;
|
||||
return { varName: left.text, typeName: right.text };
|
||||
}
|
||||
|
||||
// Null-check narrowing: x !== null, x != null, x !== undefined, x != undefined
|
||||
const op = node.children.find(c => !c.isNamed && (c.text === '!==' || c.text === '!='));
|
||||
if (!op) return undefined;
|
||||
// binary_expression children are positional — no left/right fields
|
||||
|
||||
const left = node.namedChild(0);
|
||||
const right = node.namedChild(1);
|
||||
if (left?.type !== 'identifier' || right?.type !== 'identifier') return undefined;
|
||||
return { varName: left.text, typeName: right.text };
|
||||
if (!left || !right) return undefined;
|
||||
|
||||
// Determine which side is the variable and which is null/undefined
|
||||
let varNode: SyntaxNode | undefined;
|
||||
let isNullCheck = false;
|
||||
if (left.type === 'identifier' && NULL_CHECK_KEYWORDS.has(right.text)) {
|
||||
varNode = left;
|
||||
isNullCheck = true;
|
||||
} else if (right.type === 'identifier' && NULL_CHECK_KEYWORDS.has(left.text)) {
|
||||
varNode = right;
|
||||
isNullCheck = true;
|
||||
}
|
||||
if (!isNullCheck || !varNode) return undefined;
|
||||
|
||||
const varName = varNode.text;
|
||||
// Look up the variable's resolved type (already stripped of nullable by extractSimpleTypeName)
|
||||
const resolvedType = scopeEnv.get(varName);
|
||||
if (!resolvedType) return undefined;
|
||||
|
||||
// Check if the original declaration type was nullable by looking at the raw AST type node.
|
||||
// extractSimpleTypeName already strips nullable markers, so we need the original to know
|
||||
// if narrowing is meaningful (i.e., the variable was declared as nullable).
|
||||
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
||||
if (!declTypeNode) return undefined;
|
||||
const declText = declTypeNode.text;
|
||||
// Only narrow if the original declaration was nullable
|
||||
if (!declText.includes('null') && !declText.includes('undefined')) return undefined;
|
||||
|
||||
// Find the if-body block to scope the narrowing
|
||||
const ifBody = findIfConsequenceBlock(node);
|
||||
if (!ifBody) return undefined;
|
||||
|
||||
return {
|
||||
varName,
|
||||
typeName: resolvedType,
|
||||
narrowingRange: { startIndex: ifBody.startIndex, endIndex: ifBody.endIndex },
|
||||
};
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
|
|
|
|||
|
|
@ -890,15 +890,6 @@ const processFileGroup = (
|
|||
result.fileCount++;
|
||||
onFileProcessed?.();
|
||||
|
||||
// Build per-file type environment + constructor bindings in a single AST walk.
|
||||
// Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
|
||||
const typeEnv = buildTypeEnv(tree, language);
|
||||
const callRouter = callRouters[language];
|
||||
|
||||
if (typeEnv.constructorBindings.length > 0) {
|
||||
result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
|
||||
}
|
||||
|
||||
let matches;
|
||||
try {
|
||||
matches = query.matches(tree.rootNode);
|
||||
|
|
@ -907,6 +898,39 @@ const processFileGroup = (
|
|||
continue;
|
||||
}
|
||||
|
||||
// Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
|
||||
// Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
|
||||
// in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
|
||||
// runs. This pre-pass makes parent class information available for type resolution.
|
||||
const fileParentMap = new Map<string, string[]>();
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
for (const c of match.captures) {
|
||||
captureMap[c.name] = c.node;
|
||||
}
|
||||
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
||||
const className: string = captureMap['heritage.class'].text;
|
||||
const parentName: string = captureMap['heritage.extends'].text;
|
||||
// Skip Go named fields (only anonymous fields are struct embedding)
|
||||
const extendsNode = captureMap['heritage.extends'];
|
||||
const fieldDecl = extendsNode.parent;
|
||||
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name')) continue;
|
||||
let parents = fileParentMap.get(className);
|
||||
if (!parents) { parents = []; fileParentMap.set(className, parents); }
|
||||
if (!parents.includes(parentName)) parents.push(parentName);
|
||||
}
|
||||
}
|
||||
|
||||
// Build per-file type environment + constructor bindings in a single AST walk.
|
||||
// Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
|
||||
const parentMap: ReadonlyMap<string, readonly string[]> = fileParentMap;
|
||||
const typeEnv = buildTypeEnv(tree, language, { parentMap });
|
||||
const callRouter = callRouters[language];
|
||||
|
||||
if (typeEnv.constructorBindings.length > 0) {
|
||||
result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
for (const c of match.captures) {
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/A.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
#include "Greeting.h"
|
||||
|
||||
class A {
|
||||
public:
|
||||
Greeting greet() { return Greeting(); }
|
||||
};
|
||||
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/B.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include "A.h"
|
||||
|
||||
class B : public A {};
|
||||
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/C.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include "B.h"
|
||||
|
||||
class C : public B {};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/Greeting.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class Greeting {
|
||||
public:
|
||||
void save() {}
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-grandparent-resolution/src/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "C.h"
|
||||
|
||||
void process() {
|
||||
C c;
|
||||
c.greet().save();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/A.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class A
|
||||
{
|
||||
public Greeting Greet() => new Greeting();
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/B.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class B : A {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/C.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class C : B {}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Models/Greeting.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Grandparent.Models
|
||||
{
|
||||
public class Greeting
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-grandparent-resolution/Services/App.cs
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Grandparent.Models;
|
||||
|
||||
namespace Grandparent.Services
|
||||
{
|
||||
public class App
|
||||
{
|
||||
public void Process()
|
||||
{
|
||||
var c = new C();
|
||||
c.Greet().Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace NullCheck.Models
|
||||
{
|
||||
public class Repo
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace NullCheck.Models
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public void Save() {}
|
||||
}
|
||||
}
|
||||
44
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs
vendored
Normal file
44
gitnexus/test/fixtures/lang-resolution/csharp-null-check-narrowing/Services/App.cs
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using NullCheck.Models;
|
||||
using System;
|
||||
|
||||
namespace NullCheck.Services
|
||||
{
|
||||
public class App
|
||||
{
|
||||
public App(User? x)
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
x.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessInequality(User x)
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
x.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessIsNotNull(User x)
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
x.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessInLambda(User? x)
|
||||
{
|
||||
Action act = () =>
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
x.Save();
|
||||
}
|
||||
};
|
||||
act();
|
||||
}
|
||||
}
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/go-inc-dec-write-access/main.go
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
type Counter struct {
|
||||
Count int
|
||||
Total int
|
||||
}
|
||||
|
||||
func increment(c *Counter) {
|
||||
c.Count++
|
||||
c.Total++
|
||||
}
|
||||
|
||||
func decrement(c *Counter) {
|
||||
c.Count--
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/A.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class A {
|
||||
public Greeting greet() { return new Greeting(); }
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/B.java
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models;
|
||||
|
||||
public class B extends A {}
|
||||
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/C.java
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models;
|
||||
|
||||
public class C extends B {}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/models/Greeting.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class Greeting {
|
||||
public void save() {}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-grandparent-resolution/src/services/App.java
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package services;
|
||||
|
||||
import models.C;
|
||||
|
||||
public class App {
|
||||
public void process() {
|
||||
C c = new C();
|
||||
c.greet().save();
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/app.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const { getUsers } = require('./models');
|
||||
|
||||
function process() {
|
||||
const users = getUsers();
|
||||
for (const u of users) {
|
||||
u.save();
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/js-fixpoint-for-loop/models.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @class
|
||||
*/
|
||||
class User {
|
||||
save() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {User[]}
|
||||
*/
|
||||
function getUsers() { return []; }
|
||||
|
||||
module.exports = { User, getUsers };
|
||||
7
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/app.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const { getUser } = require('./service');
|
||||
|
||||
function processDestructured() {
|
||||
const user = getUser();
|
||||
const { address } = user;
|
||||
address.save();
|
||||
}
|
||||
19
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/models.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @class
|
||||
*/
|
||||
class Address {
|
||||
/** @returns {boolean} */
|
||||
save() { return true; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
class User {
|
||||
constructor() {
|
||||
/** @type {Address} */
|
||||
this.address = new Address();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Address, User };
|
||||
8
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/js-object-destructuring/service.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const { User } = require('./models');
|
||||
|
||||
/**
|
||||
* @returns {User}
|
||||
*/
|
||||
function getUser() { return new User(); }
|
||||
|
||||
module.exports = { getUser };
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/A.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
open class A {
|
||||
fun greet(): Greeting = Greeting()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/B.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models
|
||||
|
||||
open class B : A()
|
||||
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/C.kt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package models
|
||||
|
||||
class C : B()
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/models/Greeting.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
class Greeting {
|
||||
fun save() {}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/kotlin-grandparent-resolution/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package services
|
||||
|
||||
import models.C
|
||||
|
||||
fun process() {
|
||||
val c = C()
|
||||
c.greet().save()
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/Repo.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
class Repo {
|
||||
fun save() {}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/models/User.kt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
class User {
|
||||
fun save() {}
|
||||
}
|
||||
|
||||
fun findUser(): User? {
|
||||
return null
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/kotlin-null-check-narrowing/services/App.kt
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package services
|
||||
|
||||
import models.User
|
||||
import models.findUser
|
||||
|
||||
fun processNullable(x: User?) {
|
||||
if (x != null) {
|
||||
x.save()
|
||||
}
|
||||
}
|
||||
|
||||
fun processLocalNullable() {
|
||||
val x: User? = findUser()
|
||||
if (x != null) {
|
||||
x.save()
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/A.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class A
|
||||
{
|
||||
public function greet(): Greeting { return new Greeting(); }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/B.php
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/B.php
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class B extends A {}
|
||||
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/C.php
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/C.php
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class C extends B {}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/Greeting.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Models/Greeting.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Greeting
|
||||
{
|
||||
public function save(): void {}
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Services/App.php
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/app/Services/App.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\C;
|
||||
|
||||
class App
|
||||
{
|
||||
public function process(): void
|
||||
{
|
||||
$c = new C();
|
||||
$c->greet()->save();
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/php-grandparent-resolution/composer.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/app.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from models.c import C
|
||||
|
||||
def process():
|
||||
c = C()
|
||||
c.greet().save()
|
||||
0
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/__init__.py
vendored
Normal file
0
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/__init__.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/a.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .greeting import Greeting
|
||||
|
||||
class A:
|
||||
def greet(self) -> Greeting:
|
||||
return Greeting()
|
||||
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/b.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .a import A
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/c.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .b import B
|
||||
|
||||
class C(B):
|
||||
pass
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-grandparent-resolution/models/greeting.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Greeting:
|
||||
def save(self) -> None:
|
||||
pass
|
||||
6
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/app.rb
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
require_relative 'models/c'
|
||||
|
||||
def process
|
||||
c = C.new
|
||||
c.greet.save
|
||||
end
|
||||
7
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/a.rb
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require_relative 'greeting'
|
||||
|
||||
class A
|
||||
def greet
|
||||
Greeting.new
|
||||
end
|
||||
end
|
||||
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/b.rb
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
require_relative 'a'
|
||||
|
||||
class B < A
|
||||
end
|
||||
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/c.rb
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
require_relative 'b'
|
||||
|
||||
class C < B
|
||||
end
|
||||
5
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ruby-grandparent-resolution/lib/models/greeting.rb
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class Greeting
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
12
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/main.rs
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
mod point;
|
||||
mod vec2;
|
||||
|
||||
use crate::point::Point;
|
||||
|
||||
fn process(p: Point) {
|
||||
let Point { x, y } = p;
|
||||
x.save();
|
||||
y.save();
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
6
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/point.rs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use crate::vec2::Vec2;
|
||||
|
||||
pub struct Point {
|
||||
pub x: Vec2,
|
||||
pub y: Vec2,
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-struct-destructuring/vec2.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub struct Vec2 {
|
||||
pub value: f32,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn save(&self) {}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { getUsers } from './models';
|
||||
|
||||
function process() {
|
||||
const users = getUsers();
|
||||
for (const u of users) {
|
||||
u.save();
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ts-fixpoint-for-loop/src/models.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export class User {
|
||||
save(): void {}
|
||||
}
|
||||
export function getUsers(): User[] {
|
||||
return [];
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { C } from './derived';
|
||||
|
||||
function process() {
|
||||
const c = new C();
|
||||
c.greet().save();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/base.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Greeting } from './greeting';
|
||||
|
||||
export class A {
|
||||
greet(): Greeting { return new Greeting(); }
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/derived.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { B } from './middle';
|
||||
export class C extends B {}
|
||||
3
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/greeting.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export class Greeting {
|
||||
save(): void {}
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/ts-grandparent-resolution/src/middle.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { A } from './base';
|
||||
export class B extends A {}
|
||||
25
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts
vendored
Normal file
25
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { User } from './models';
|
||||
|
||||
function processStrict(x: User | null) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
|
||||
function processLoose(x: User | null) {
|
||||
if (x != null) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
|
||||
function processUndefined(x: User | undefined) {
|
||||
if (x !== undefined) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
|
||||
const processFuncExpr = function(x: User | null) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
}
|
||||
};
|
||||
7
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ts-null-check-narrowing/src/models.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export class User {
|
||||
save(): void {}
|
||||
}
|
||||
|
||||
export class Repo {
|
||||
save(): void {}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/app.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { getUser } from './service';
|
||||
|
||||
function processDestructured() {
|
||||
const user = getUser();
|
||||
const { address } = user;
|
||||
address.save();
|
||||
}
|
||||
|
||||
function processMultiField() {
|
||||
const user = getUser();
|
||||
const { name, address } = user;
|
||||
address.save();
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/models.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export class Address {
|
||||
city: string = '';
|
||||
save(): boolean { return true; }
|
||||
}
|
||||
|
||||
export class User {
|
||||
name: string = '';
|
||||
address: Address = new Address();
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/ts-object-destructuring/src/service.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { User } from './models';
|
||||
export function getUser(): User { return new User(); }
|
||||
|
|
@ -1015,3 +1015,49 @@ describe('C++ method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.h'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1362,3 +1362,110 @@ describe('C# method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// Greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.Greet().Save() to Greeting#Save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.Greet() to A#Greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'Greet' && c.targetFilePath.includes('A.cs'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: C# null-check narrowing — if (x != null) and if (x is not null)
|
||||
// Both patterns emit patternOverrides for the if-body position range
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# null-check narrowing resolution (Phase C)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-null-check-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves x.Save() inside != null guard (ProcessInequality) to User#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessInequality' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves x.Save() inside is not null guard (ProcessIsNotNull) to User#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessIsNotNull' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT cross-resolve to Repo#Save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongCall = calls.find(c =>
|
||||
c.target === 'Save' && c.targetFilePath.includes('Repo'),
|
||||
);
|
||||
expect(wrongCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves x.Save() inside constructor via null-check narrowing', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'App' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves x.Save() inside lambda via null-check narrowing', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'Save' && c.source === 'ProcessInLambda' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1145,3 +1145,40 @@ describe('Go method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Go inc_statement / dec_statement write access
|
||||
// obj.Field++ and obj.Field-- emit ACCESSES write edges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Go inc/dec write access tracking (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'go-inc-dec-write-access'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('emits ACCESSES write edge for Count++ in increment', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const writes = accesses.filter(e => e.rel.reason === 'write');
|
||||
const countInc = writes.find(e => e.target === 'Count' && e.source === 'increment');
|
||||
expect(countInc).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits ACCESSES write edge for Total++ in increment', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const writes = accesses.filter(e => e.rel.reason === 'write');
|
||||
const totalInc = writes.find(e => e.target === 'Total' && e.source === 'increment');
|
||||
expect(totalInc).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits ACCESSES write edge for Count-- in decrement', () => {
|
||||
const accesses = getRelationships(result, 'ACCESSES');
|
||||
const writes = accesses.filter(e => e.rel.reason === 'write');
|
||||
const countDec = writes.find(e => e.target === 'Count' && e.source === 'decrement');
|
||||
expect(countDec).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1278,3 +1278,49 @@ describe('Java method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Java grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.java'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -302,3 +302,55 @@ describe('Write access tracking (JavaScript)', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: JS object destructuring — const { field } = receiver → fieldAccess PendingAssignment
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('JavaScript object destructuring resolution (Phase A)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'js-object-destructuring'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User, Address classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('User');
|
||||
expect(classes).toContain('Address');
|
||||
});
|
||||
|
||||
it('resolves address.save() to Address#save via object destructuring', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Post-fixpoint for-loop replay — iterable resolved via callResult fixpoint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('JavaScript post-fixpoint for-loop replay (Phase A ex-9B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'js-fixpoint-for-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves u.save() to User#save via post-fixpoint for-loop replay', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1441,3 +1441,94 @@ describe('Kotlin method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.kt'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: Kotlin null-check narrowing — if (x != null) { x.save() }
|
||||
// NOTE: depends on nullable_type capture being fixed in jvm.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin null-check narrowing resolution (Phase C)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-null-check-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves x.save() inside != null guard to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processNullable' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve to Repo#save (no cross-contamination)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Repo'),
|
||||
);
|
||||
expect(wrongCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves x.save() from local variable val x: User? via null-check narrowing', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processLocalNullable' && c.targetFilePath.includes('User'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1382,3 +1382,49 @@ describe('PHP method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PHP grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'php-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves $c->greet()->save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('Greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves $c->greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('A.php'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1428,3 +1428,49 @@ describe('Python method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('a.py'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1012,3 +1012,49 @@ describe('Ruby method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects A, B, C, Greeting classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet.save to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('a.rb'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1475,3 +1475,40 @@ describe('Rust method chain binding via unified fixpoint (Phase 9C)', () => {
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Rust struct_pattern destructuring — let Point { x, y } = p
|
||||
// Each field emits a fieldAccess PendingAssignment; fixpoint resolves x/y → Vec2
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust struct_pattern destructuring resolution (Phase A)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-struct-destructuring'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Point and Vec2 structs', () => {
|
||||
const classes = getNodesByLabel(result, 'Struct');
|
||||
expect(classes).toContain('Point');
|
||||
expect(classes).toContain('Vec2');
|
||||
});
|
||||
|
||||
it('resolves x.save() to Vec2#save via struct destructuring', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('vec2'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves both x.save() and y.save() — emits at least 1 CALLS to Vec2#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(c => c.target === 'save' && c.targetFilePath.includes('vec2'));
|
||||
// Both x and y are Vec2 — the same function, so calls may deduplicate to 1
|
||||
expect(saveCalls.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2054,3 +2054,171 @@ describe('TypeScript method chain binding via unified fixpoint (Phase 9C)', () =
|
|||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Object destructuring — const { field } = receiver → fieldAccess PendingAssignment
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript object destructuring resolution (Phase A)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-object-destructuring'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User, Address classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('User');
|
||||
expect(classes).toContain('Address');
|
||||
});
|
||||
|
||||
it('resolves address.save() to Address#save via object destructuring', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve save() to a wrong target', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(c => c.target === 'save');
|
||||
for (const call of saveCalls) {
|
||||
expect(call.targetFilePath).toContain('models');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase A: Post-fixpoint for-loop replay — iterable resolved via callResult fixpoint
|
||||
// Differs from ts-for-of-call-expression: iterable is an identifier, not inline call
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript post-fixpoint for-loop replay (Phase A ex-9B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-fixpoint-for-loop'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves u.save() to User#save via post-fixpoint for-loop replay', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
|
||||
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript grandparent method resolution via MRO (Phase B)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-grandparent-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects 3 classes in inheritance chain (A, B, C) plus Greeting', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('A');
|
||||
expect(classes).toContain('B');
|
||||
expect(classes).toContain('C');
|
||||
expect(classes).toContain('Greeting');
|
||||
});
|
||||
|
||||
it('emits EXTENDS edges: B→A, C→B', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(edgeSet(extends_)).toContain('B → A');
|
||||
expect(edgeSet(extends_)).toContain('C → B');
|
||||
});
|
||||
|
||||
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetFilePath.includes('greeting'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const greetCall = calls.find(c =>
|
||||
c.target === 'greet' && c.targetFilePath.includes('base'),
|
||||
);
|
||||
expect(greetCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase C: TS null-check narrowing — if (x !== null) { x.save() }
|
||||
// patternOverrides stores narrowed type for the if-body position range
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TypeScript null-check narrowing resolution (Phase C)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ts-null-check-narrowing'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
});
|
||||
|
||||
it('resolves x.save() inside !== null guard to User#save', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processStrict' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('does NOT resolve to Repo#save (no cross-contamination)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const wrongCall = calls.find(c =>
|
||||
c.target === 'save' && c.targetLabel === 'Repo',
|
||||
);
|
||||
expect(wrongCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves x.save() in loose != null check (processLoose)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processLoose' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves x.save() in !== undefined check (processUndefined)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processUndefined' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves x.save() inside function expression null-check (processFuncExpr)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c =>
|
||||
c.target === 'save' && c.source === 'processFuncExpr' && c.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(saveCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1589,7 +1589,7 @@ class RepoService {
|
|||
getStats: () => ({ fileCount: 0, globalSymbolCount: 0 }),
|
||||
clear: () => {},
|
||||
};
|
||||
const { env } = buildTypeEnv(tree, 'kotlin', mockSymbolTable as any);
|
||||
const { env } = buildTypeEnv(tree, 'kotlin', { symbolTable: mockSymbolTable as any });
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
|
||||
|
|
@ -1610,7 +1610,7 @@ class RepoService {
|
|||
getStats: () => ({ fileCount: 0, globalSymbolCount: 0 }),
|
||||
clear: () => {},
|
||||
};
|
||||
const { env } = buildTypeEnv(tree, 'kotlin', mockSymbolTable as any);
|
||||
const { env } = buildTypeEnv(tree, 'kotlin', { symbolTable: mockSymbolTable as any });
|
||||
expect(flatGet(env, 'result')).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -3694,4 +3694,162 @@ function calculate(service: Service) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('null-check narrowing via patternOverrides (Phase C Task 7)', () => {
|
||||
it('TS: if (x !== null) narrows User | null to User inside if-body', () => {
|
||||
const code = `
|
||||
function process(x: User | null) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, TypeScript.typescript);
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript');
|
||||
// Inside the if-body, x should resolve to User (nullable stripped)
|
||||
const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save'));
|
||||
expect(typeEnv.lookup('x', saveCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('TS: if (x !== undefined) narrows User | undefined to User inside if-body', () => {
|
||||
const code = `
|
||||
function process(x: User | undefined) {
|
||||
if (x !== undefined) {
|
||||
x.save();
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, TypeScript.typescript);
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript');
|
||||
const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save'));
|
||||
expect(typeEnv.lookup('x', saveCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('TS: if (x != null) narrows with loose inequality', () => {
|
||||
const code = `
|
||||
function process(x: User | null) {
|
||||
if (x != null) {
|
||||
x.save();
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, TypeScript.typescript);
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript');
|
||||
const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save'));
|
||||
expect(typeEnv.lookup('x', saveCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('TS: null-check narrowing does NOT leak to else branch', () => {
|
||||
const code = `
|
||||
function process(x: User | null) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
} else {
|
||||
x.fallback();
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, TypeScript.typescript);
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript');
|
||||
// Inside else branch, x should retain original nullable type (User via fastStripNullable)
|
||||
const fallbackCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.fallback'));
|
||||
// The else branch is NOT in the narrowing range, so lookup falls through to
|
||||
// the flat scopeEnv which has "User | null" — fastStripNullable strips it to User.
|
||||
// This is expected: without negative narrowing (Phase 13A), else branches still get
|
||||
// the base stripped type. The key invariant is that the narrowing override does NOT
|
||||
// apply outside the if-body range.
|
||||
expect(typeEnv.lookup('x', fallbackCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('TS: null-check narrowing does NOT apply outside the if block', () => {
|
||||
const code = `
|
||||
function process(x: User | null) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
}
|
||||
x.other();
|
||||
}`;
|
||||
const tree = parse(code, TypeScript.typescript);
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript');
|
||||
// After the if-block, x should use the flat scopeEnv (User | null → User via fastStripNullable)
|
||||
const otherCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.other'));
|
||||
expect(typeEnv.lookup('x', otherCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('TS: no narrowing when variable has no nullable type', () => {
|
||||
const code = `
|
||||
function process(x: User) {
|
||||
if (x !== null) {
|
||||
x.save();
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, TypeScript.typescript);
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript');
|
||||
// x is already non-nullable — no narrowing override is emitted, but lookup still works
|
||||
const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save'));
|
||||
expect(typeEnv.lookup('x', saveCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('TS: instanceof still works alongside null-check narrowing', () => {
|
||||
const tree = parse(`
|
||||
function process(x) {
|
||||
if (x instanceof User) {
|
||||
x.save();
|
||||
}
|
||||
}
|
||||
`, TypeScript.typescript);
|
||||
const { env } = buildTypeEnv(tree, 'typescript');
|
||||
expect(flatGet(env, 'x')).toBe('User');
|
||||
});
|
||||
|
||||
it('Kotlin: if (x != null) narrows nullable type inside if-body', () => {
|
||||
const code = `
|
||||
fun process(x: User?) {
|
||||
if (x != null) {
|
||||
x.save()
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, Kotlin);
|
||||
const typeEnv = buildTypeEnv(tree, 'kotlin');
|
||||
const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.save'));
|
||||
expect(typeEnv.lookup('x', saveCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('Kotlin: when/is still works alongside null-check narrowing', () => {
|
||||
const tree = parse(`
|
||||
fun process(x: Any) {
|
||||
when (x) {
|
||||
is User -> x.name
|
||||
}
|
||||
}
|
||||
`, Kotlin);
|
||||
const { env } = buildTypeEnv(tree, 'kotlin');
|
||||
expect(flatGet(env, 'x')).toBe('User');
|
||||
});
|
||||
|
||||
it('C#: if (x != null) narrows nullable type inside if-body', () => {
|
||||
const code = `
|
||||
class App {
|
||||
void Process(User? x) {
|
||||
if (x != null) {
|
||||
x.Save();
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const tree = parse(code, CSharp);
|
||||
const typeEnv = buildTypeEnv(tree, 'csharp');
|
||||
const saveCall = tree.rootNode.descendantForIndex(tree.rootNode.text.indexOf('x.Save'));
|
||||
expect(typeEnv.lookup('x', saveCall)).toBe('User');
|
||||
});
|
||||
|
||||
it('C#: is_pattern_expression type pattern still works alongside null-check', () => {
|
||||
const tree = parse(`
|
||||
class App {
|
||||
void Process(object obj) {
|
||||
if (obj is User user) {
|
||||
user.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
`, CSharp);
|
||||
const { env } = buildTypeEnv(tree, 'csharp');
|
||||
expect(flatGet(env, 'user')).toBe('User');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,122 +51,79 @@ The goal is not to build a compiler. The goal is to support high-value static an
|
|||
- Fixpoint iterates until stable (max 10 iterations), enabling chains like `getUser() → .address → .getCity() → city.save()`
|
||||
- Reverse-order copy chains now resolve (`const b = a; const a: User = x` → both resolve)
|
||||
|
||||
### Milestone D — Completeness ✅
|
||||
|
||||
**Shipped in** `feat/type-resolution-milestone-d` (PR #387). Consolidated original Phases 10–13 into 3 balanced phases.
|
||||
|
||||
#### Phase A: Fixpoint Completeness ✅
|
||||
|
||||
- Post-fixpoint for-loop replay (ex-9B): `pendingForLoops` collection + replay after fixpoint resolves iterable types
|
||||
- Object destructuring via `fieldAccess` items (TS/JS `object_pattern`, Rust `struct_pattern`) — no new `destructure` PendingAssignment variant needed
|
||||
- Extracted `resolveFixpointBindings()` helper with exhaustive switch + `classDefCache` memoization
|
||||
|
||||
#### Phase B: Inheritance & Receivers ✅
|
||||
|
||||
- `BuildTypeEnvOptions` interface replacing positional params for `buildTypeEnv`
|
||||
- Heritage pre-pass constructing `parentMap` from tree-sitter query matches (not graph edges — heritage-processor runs in parallel)
|
||||
- MRO-aware `walkParentChain()` (depth 5, cycle-safe BFS) for `resolveFieldType` and `resolveMethodReturnType`
|
||||
- `this`/`self`/`$this`/`Me` receiver substitution via `substituteThisReceiver` hook
|
||||
- Go `inc_statement`/`dec_statement` write-access queries
|
||||
|
||||
#### Phase C: Branch-Sensitive Narrowing ✅
|
||||
|
||||
- Null-check narrowing (`!= null`, `!== undefined`, `is not null`) via position-indexed `patternOverrides`
|
||||
- Supported for TS, Kotlin, C# — renamed `PATTERN_BRANCH_TYPES` → `NARROWING_BRANCH_TYPES`
|
||||
- Bug fix: Kotlin narrowing required 3 fixes in `jvm.ts` (AST node type `equality_expression`, anonymous `null` node, `nullable_type` parameter fallback)
|
||||
|
||||
#### Deferred from Milestone D
|
||||
|
||||
- **Type predicates (13A):** Cross-function analysis for niche TS `x is User` feature — deferred
|
||||
- **Swift parity (11D):** tree-sitter-swift Node 22 issues — all Swift work consolidated to Phase S
|
||||
- **Positional destructuring (12C):** Python/Kotlin/C#/C++ tuple-position-to-field mapping — deferred
|
||||
- **Discriminated union narrowing (13C):** Needs tagged union metadata not in SymbolTable — deferred
|
||||
|
||||
#### Integration Test Coverage
|
||||
|
||||
17 fixture directories, 23 describe blocks, 705 lines of test code covering all 11 languages:
|
||||
- Grandparent MRO (depth-2 C→B→A): TS, JS, Kotlin, C#, C++, Java, PHP, Python, Ruby
|
||||
- Object destructuring: TS, JS
|
||||
- Struct destructuring: Rust
|
||||
- Post-fixpoint for-loop replay: TS, JS
|
||||
- Go inc/dec write access
|
||||
- Null-check narrowing: TS, C#, Kotlin
|
||||
|
||||
---
|
||||
|
||||
## Open Phases
|
||||
|
||||
### Phase 10: Loop-Fixpoint Bridge
|
||||
### Phase P: Polymorphism & Overloading
|
||||
|
||||
**Supersedes Phase 9B.** For-loop element bindings run during `walk()` before the fixpoint. Variables typed by the fixpoint are invisible to for-loop extraction.
|
||||
**Plan:** `docs/plans/2026-03-19-feat-polymorphism-overloading-type-resolution-plan.md`
|
||||
|
||||
**Problem:**
|
||||
```typescript
|
||||
const users = getUsers(); // fixpoint: users → User[]
|
||||
for (const u of users) { // walk-time: users untyped → u unresolved
|
||||
u.save(); // missed CALLS edge
|
||||
}
|
||||
```
|
||||
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
|
||||
4. **Covariant return type awareness** — prefer child's return type over inherited definition
|
||||
|
||||
**Approach:** Post-fixpoint for-loop replay. During `walk()`, store for-loop AST nodes whose iterable is unresolved. After fixpoint completes, replay `extractForLoopBinding` on those nodes using the now-resolved iterable types.
|
||||
Languages benefiting: Java, Kotlin, C#, C++, TypeScript (overloading). All OOP languages (virtual dispatch).
|
||||
|
||||
**Scope:**
|
||||
- Infrastructure: `pendingForLoops` collection + replay (~20 lines in `type-env.ts`)
|
||||
- No extractor changes — reuses existing `extractForLoopBinding`
|
||||
- Swift: add for-loop element binding (currently missing)
|
||||
- Nested for-loops with field-dependent iterables are NOT replayed (handled by call-processor chain resolution)
|
||||
|
||||
**Risks:** AST node lifetime (safe — nodes valid within `buildTypeEnv` lifetime).
|
||||
|
||||
**Impact: High | Effort: Low-Medium**
|
||||
**Impact: High | Effort: High**
|
||||
|
||||
---
|
||||
|
||||
### Phase 11: Inheritance & this/self
|
||||
### Phase S: Swift Parity
|
||||
|
||||
Four items sharing infrastructure.
|
||||
**Blocked on** tree-sitter-swift Node 22 compatibility.
|
||||
|
||||
#### 11A: MRO-aware field and method lookups
|
||||
|
||||
**Problem:** `lookupFieldByOwner` only finds direct fields. Inherited fields (`Admin extends User`, field on `User`) don't resolve.
|
||||
|
||||
**Approach:** Pre-compute `parentMap: Map<nodeId, nodeId[]>` from EXTENDS + IMPLEMENTS edges. Pass to `buildTypeEnv`. Update `resolveFieldType` and `resolveMethodReturnType` to walk parent chain on miss (max depth 5, cycle-safe, first-match-wins for diamond inheritance).
|
||||
|
||||
Includes IMPLEMENTS edges so interface-declared fields/methods resolve (Java, Kotlin, C#).
|
||||
|
||||
#### 11B: this/self in fixpoint
|
||||
|
||||
**Problem:** `this.field` and `this.method()` emit pending items with `receiver: 'this'`, but `scopeEnv.get('this')` returns `undefined`.
|
||||
|
||||
**Approach:** At collection time during `walk()`, resolve the enclosing class name immediately via `findEnclosingClassName()` and substitute it as the receiver. Covers both `fieldAccess` and `methodCallResult`. No fixpoint changes needed. 5-10 lines per extractor.
|
||||
|
||||
#### 11C: Go inc/dec write access
|
||||
|
||||
**Problem:** `obj.field++`/`obj.field--` produce `inc_statement`/`dec_statement` — write-access tracking doesn't see them.
|
||||
|
||||
**Approach:** Add these node types to call-processor write detection (~5 lines).
|
||||
|
||||
#### 11D: Swift assignment chains
|
||||
|
||||
**Problem:** Swift has no `extractPendingAssignment` — copy/callResult/fieldAccess/methodCallResult don't work.
|
||||
|
||||
**Approach:** Implement `extractPendingAssignment` for Swift covering all 4 binding kinds.
|
||||
|
||||
**Risks:** Performance of parent chain walking in fixpoint (bounded by `n_pending × depth × iterations`). Interface method ambiguity (mitigated by checking direct class first, parents on miss).
|
||||
|
||||
**Impact: Medium-High | Effort: Medium**
|
||||
|
||||
---
|
||||
|
||||
### Phase 12: Destructuring
|
||||
|
||||
**Problem:** `const { address, name } = user` produces no bindings — LHS is a pattern node, not an identifier.
|
||||
|
||||
**Approach:** Add `{ kind: 'destructure', source, bindings: [{ varName, fieldName }] }` to `PendingAssignment`. In the fixpoint, when source's type resolves, look up each field via `lookupFieldByOwner` and bind each variable.
|
||||
|
||||
Works without Phase 11A (direct fields only). Phase 11A MRO enhances it to resolve inherited fields too.
|
||||
|
||||
**Phased delivery:**
|
||||
|
||||
| Sub-phase | Scope | Languages |
|
||||
|-----------|-------|-----------|
|
||||
| **12A** | Object destructuring | TS/JS (`object_pattern`) |
|
||||
| **12B** | Struct pattern destructuring | Rust (`struct_pattern`) |
|
||||
| **12C** (deferred) | Positional destructuring | Python, Kotlin, C#, C++ — needs tuple-position-to-field mapping |
|
||||
|
||||
Skip: computed properties, rest elements, nested destructuring (`{ address: { city } }` — deferred).
|
||||
|
||||
**Risks:** Nested destructuring requires recursive resolution (explicitly deferred).
|
||||
- For-loop element binding (from Phase 10)
|
||||
- Assignment chains: copy, callResult, fieldAccess, methodCallResult (from Phase 11D)
|
||||
- `guard let` narrowing (from Phase 13B) — uses scopeEnv path, not `patternOverrides`
|
||||
|
||||
**Impact: Medium | Effort: Medium**
|
||||
|
||||
---
|
||||
|
||||
### Phase 13: Branch-Sensitive Narrowing
|
||||
|
||||
**Design principle:** Targeted narrowing, not general control-flow analysis. Skip anything that requires a control-flow graph.
|
||||
|
||||
**Phased delivery:**
|
||||
|
||||
#### 13A: Type predicate functions (TS only)
|
||||
|
||||
`function isUser(x: unknown): x is User` — detect `type_predicate` return type. When called in an `if` condition, emit pattern binding for the narrowed parameter.
|
||||
|
||||
#### 13B: Nullability narrowing (TS/Kotlin/C#/Swift)
|
||||
|
||||
`if (x != null)` → strip nullable wrapper in truthy branch. Uses existing `patternOverrides` mechanism (position-indexed, scope-aware). Swift `guard let` uses standard scopeEnv (narrowing persists for rest of function). Swift work requires Phase 11D (assignment chains) first.
|
||||
|
||||
#### 13C: Discriminated union narrowing (deferred)
|
||||
|
||||
`if (shape.kind === 'circle')` → needs tagged union metadata not in SymbolTable. Defer.
|
||||
|
||||
**What we skip entirely:** Full control-flow graph, arbitrary conditional narrowing, `typeof` guards, exhaustiveness checking.
|
||||
|
||||
**Risks:** Scope leakage (mitigated by `patternOverrides` position indexing). Swift `guard let` needs scopeEnv path (different from `patternOverrides`).
|
||||
|
||||
**Impact: Medium | Effort: Medium-High**
|
||||
|
||||
---
|
||||
|
||||
### Phase 14: Cross-File Binding Propagation
|
||||
|
||||
**Problem:** `buildTypeEnv` is per-file. Inferred types don't cross file boundaries.
|
||||
|
|
@ -202,19 +159,14 @@ config.validate(); // missed
|
|||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 10 (loops) ──────────────────────┐
|
||||
│
|
||||
Phase 11 (MRO + this + Go + Swift) ───┤
|
||||
├──→ Phase 14 (cross-file)
|
||||
Phase 12 (destructuring) ─────────────┤
|
||||
│
|
||||
Phase 13 (branch narrowing) ───────────┘
|
||||
Milestone D (Phases A, B, C) ✅ ──┐
|
||||
├──→ Phase 14 (cross-file)
|
||||
Phase P (polymorphism) ───────────┤
|
||||
│
|
||||
Phase S (Swift parity) ───────────┘
|
||||
|
||||
Phases 10–13 are independent of each other.
|
||||
Exception: Phase 13B Swift (guard let) requires Phase 11D (Swift assignment chains).
|
||||
Exception: Phase 12 benefits from Phase 11A (MRO) but works without it.
|
||||
Phase 14 depends on all of 10–13 being stable.
|
||||
Swift parity threaded through Phases 10–13 incrementally.
|
||||
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).
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -222,19 +174,11 @@ Swift parity threaded through Phases 10–13 incrementally.
|
|||
## Language-Specific Gaps (remaining)
|
||||
|
||||
### Swift
|
||||
- For-loop element binding → Phase 10
|
||||
- Assignment chains (copy, callResult, fieldAccess, methodCallResult) → Phase 11D
|
||||
- Pattern binding → Phase 13B (`guard let`)
|
||||
|
||||
### Go
|
||||
- `obj.field++`/`obj.field--` write ACCESSES → Phase 11C
|
||||
|
||||
### Rust
|
||||
- Struct-pattern field destructuring → Phase 12B
|
||||
- For-loop element binding → Phase S
|
||||
- Assignment chains (copy, callResult, fieldAccess, methodCallResult) → Phase S
|
||||
- `guard let` narrowing → Phase S
|
||||
|
||||
### All languages
|
||||
- Inherited field/method resolution → Phase 11A
|
||||
- `this`/`self` in fixpoint → Phase 11B
|
||||
- Cross-file binding propagation → Phase 14
|
||||
|
||||
---
|
||||
|
|
@ -253,14 +197,22 @@ Field/property maps, deep chains, mixed chains, stdlib passthroughs.
|
|||
|
||||
Unified fixpoint loop, call-result binding, field access binding, method-call-result binding, arbitrary-depth chain propagation.
|
||||
|
||||
### Milestone D — Completeness ← **next** (Phases 10–13)
|
||||
### Milestone D — Completeness ✅ (Phases A, B, C)
|
||||
|
||||
Loop-fixpoint bridge, inheritance walking, `this`/`self` resolution, destructuring, branch narrowing, Swift parity.
|
||||
Consolidated Phases 10–13 into 3 balanced phases. Loop-fixpoint bridge, MRO-aware inheritance walking, `this`/`self` resolution, object/struct destructuring, null-check narrowing. Kotlin null-check bug fix. Full 11-language integration test coverage.
|
||||
|
||||
### Milestone E — Cross-Boundary (Phase 14)
|
||||
### Milestone E — Cross-Boundary ← **next** (Phase 14)
|
||||
|
||||
Export-type index, cross-file binding propagation.
|
||||
|
||||
### Milestone P — Polymorphism & Overloading (Phase P)
|
||||
|
||||
Parameter type metadata, overload disambiguation, constructor-visible virtual dispatch, covariant return types.
|
||||
|
||||
### Milestone S — Swift Parity (Phase S)
|
||||
|
||||
For-loop binding, assignment chains, `guard let` narrowing. Blocked on tree-sitter-swift Node 22.
|
||||
|
||||
---
|
||||
|
||||
## Open Design Questions
|
||||
|
|
@ -294,8 +246,6 @@ That supports: better call graphs, more accurate impact analysis, stronger AI co
|
|||
|
||||
## Summary
|
||||
|
||||
**Complete:** Phases 7, 8, 9, 9C — explicit types, constructor inference, loop inference, field/property resolution, deep chains, mixed chains, stdlib passthroughs, comment-based types, unified fixpoint with 4 binding kinds, arbitrary-depth chain propagation across 11 languages.
|
||||
**Complete:** Phases 7, 8, 9, 9C, Milestone D (A, B, C) — explicit types, constructor inference, loop inference, field/property resolution, deep chains, mixed chains, stdlib passthroughs, comment-based types, unified fixpoint with 4 binding kinds, arbitrary-depth chain propagation, MRO-aware inheritance walking, this/self resolution, object/struct destructuring, null-check narrowing — across 11 languages with full integration test coverage.
|
||||
|
||||
**Next:** Phase 10 (loop-fixpoint bridge) → Phase 11 (MRO + this/self + Go + Swift) → Phase 12 (destructuring) → Phase 13 (branch narrowing) → Phase 14 (cross-file propagation).
|
||||
|
||||
Each phase is independently deliverable (except Phase 14 which depends on 10–13 being stable). Swift parity is threaded incrementally through Phases 10–13.
|
||||
**Next:** Phase 14 (cross-file binding propagation) — the architectural capstone. Phase S (Swift parity) is independent and unblocked once tree-sitter-swift Node 22 is resolved.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue