fix(type-env): thread enclosingFunctionFinder through scope resolution, unskip Dart ACCESSES test

The type-env's findEnclosingScopeKey had the same Dart sibling problem
as findEnclosingFunction — it walked parents but never found
function_signature because the call lives inside function_body (a
sibling).  Instead of hardcoding a function_body check, thread the
provider's enclosingFunctionFinder hook through BuildTypeEnvOptions →
lookupInEnv → findEnclosingScopeKey.  All three buildTypeEnv call sites
(call-processor, parsing-processor, parse-worker) now pass the hook.

This enables the type-env to resolve scoped parameter bindings for Dart
(e.g., `user: User` in processUser), which lets the chain-resolution
tier (Step 1c) walk `user.address` and emit ACCESSES edges.

Dart integration test unskipped — 10/10 passing including ACCESSES.
Reverted CHANGELOG.md to origin/main.
This commit is contained in:
Gergo Magyar 2026-03-26 14:30:17 +00:00
parent dddfd0d789
commit fc49fd2e72
6 changed files with 32 additions and 30 deletions

View file

@ -2,18 +2,6 @@
All notable changes to GitNexus will be documented in this file.
## [Unreleased]
### Added
- Phase 8: Field and Property Type Resolution
- Field-type maps for TypeScript class/interface declarations
- Language-agnostic FieldExtractor interface and registry
- Chained property resolution support
- Phase 9: Return-Type-Aware Variable Binding
- Call-result variable binding in TypeEnv
- FQN-aware return type lookup
- Enhanced pending assignment processing
## [1.4.8] - 2026-03-23
### Added

View file

@ -395,7 +395,7 @@ export const processCalls = async (
const importedBindings = importedBindingsMap?.get(file.path);
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes });
const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
if (typeEnv && exportedTypeMap) {
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
if (fileExports) exportedTypeMap.set(file.path, fileExports);

View file

@ -263,7 +263,7 @@ const processParsingSequential = async (
}
// Build per-file type environment for FieldExtractor context (lightweight — skipped if no fieldExtractor)
const typeEnv = provider.fieldExtractor ? buildTypeEnv(tree, language) : null;
const typeEnv = provider.fieldExtractor ? buildTypeEnv(tree, language, { enclosingFunctionFinder: provider.enclosingFunctionFinder }) : null;
matches.forEach(match => {
const captureMap: Record<string, any> = {};

View file

@ -6,6 +6,7 @@ import { getProvider } from './languages/index.js';
import type { ClassNameLookup, ReturnTypeLookup, ForLoopExtractorContext, PendingAssignment } from './type-extractors/types.js';
import { extractSimpleTypeName, extractVarName, stripNullable, extractReturnTypeName } from './type-extractors/shared.js';
import type { SymbolTable } from './symbol-table.js';
import type { NodeLabel } from '../graph/types.js';
/**
* Per-file scoped type environment: maps (scope, variableName) → typeName.
@ -115,6 +116,7 @@ const lookupInEnv = (
varName: string,
callNode: SyntaxNode,
patternOverrides?: PatternOverrides,
enclosingFunctionFinder?: (n: SyntaxNode) => { funcName: string; label: NodeLabel } | null,
): string | undefined => {
// Self/this receiver: resolve to enclosing class name via AST walk
if (varName === 'self' || varName === 'this' || varName === '$this') {
@ -128,7 +130,7 @@ const lookupInEnv = (
}
// Determine the enclosing function scope for the call
const scopeKey = findEnclosingScopeKey(callNode);
const scopeKey = findEnclosingScopeKey(callNode, enclosingFunctionFinder);
// Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts).
// These take priority over flat scopeEnv because they represent per-branch narrowing.
@ -338,14 +340,30 @@ const extractParentClassFromNode = (classNode: SyntaxNode): string | undefined =
return undefined;
};
/** Find the enclosing function name for scope lookup. */
const findEnclosingScopeKey = (node: SyntaxNode): string | undefined => {
/** Find the enclosing function name for scope lookup.
* When an `enclosingFunctionFinder` hook is provided (from the language provider),
* it is consulted for each ancestor before the default FUNCTION_NODE_TYPES check.
* This handles languages like Dart where the function body is a sibling of the
* signature instead of a child. */
const findEnclosingScopeKey = (
node: SyntaxNode,
enclosingFunctionFinder?: (n: SyntaxNode) => { funcName: string; label: NodeLabel } | null,
): string | undefined => {
let current = node.parent;
while (current) {
if (FUNCTION_NODE_TYPES.has(current.type)) {
const { funcName } = extractFunctionName(current);
if (funcName) return `${funcName}@${current.startIndex}`;
}
// Language-specific hook (e.g., Dart function_body → sibling function_signature)
if (enclosingFunctionFinder) {
const result = enclosingFunctionFinder(current);
if (result) {
const sigNode = current.previousSibling;
const startIdx = sigNode?.startIndex ?? current.startIndex;
return `${result.funcName}@${startIdx}`;
}
}
current = current.parent;
}
return undefined;
@ -683,6 +701,10 @@ export interface BuildTypeEnvOptions {
* Stores raw declared return type strings (e.g., 'User[]', 'List<User>').
* Used by lookupRawReturnType for for-loop element extraction. */
importedRawReturnTypes?: ReadonlyMap<string, string>;
/** Language-specific enclosing function resolver for scope key lookup.
* Same hook as LanguageProvider.enclosingFunctionFinder — handles languages
* where function_body is a sibling of the signature (e.g., Dart). */
enclosingFunctionFinder?: (ancestorNode: SyntaxNode) => { funcName: string; label: NodeLabel } | null;
}
/** Seed cross-file type bindings into the file scope.
@ -1110,7 +1132,7 @@ export const buildTypeEnv = (
}
return {
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides, options?.enclosingFunctionFinder),
constructorBindings: bindings,
fileScope: () => env.get(FILE_SCOPE) ?? EMPTY_FILE_SCOPE,
allScopes: () => env as ReadonlyMap<string, ReadonlyMap<string, string>>,

View file

@ -1044,8 +1044,8 @@ const processFileGroup = (
// 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 provider = getProvider(language);
const typeEnv = buildTypeEnv(tree, language, { parentMap, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
const callRouter = provider.callRouter;
if (typeEnv.constructorBindings.length > 0) {

View file

@ -3,8 +3,8 @@
* Verifies that class fields are captured as Property nodes with HAS_PROPERTY
* edges, and that calls (including chained and call-result-bound) are resolved.
*
* Remaining known Dart gaps (field-chain ACCESSES) are documented as
* it.todo() tests to be filled when the pipeline is extended.
* All Dart pipeline features are covered: Property nodes, HAS_PROPERTY edges,
* CALLS chain resolution, IMPORTS, call attribution, and ACCESSES field reads.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
@ -78,15 +78,7 @@ describe.skipIf(!dartAvailable)('Dart field-type resolution', () => {
expect(appImports.length).toBe(1);
});
// Dart field-chain ACCESSES edges require the call-processor's chain-resolution
// tier (Step 1c) to fire. This needs the type-env's scoped parameter binding
// (processUser's `user: User`) to propagate to processCallsFromExtracted so
// walkMixedChain can resolve User → address → Address and emit ACCESSES.
// The chain extraction (extractMixedChain) and member detection
// (MEMBER_ACCESS_NODE_TYPES) are wired, but the base receiver type lookup
// from the type-env currently returns undefined for Dart function parameters
// in the call-processor context. Tracked for follow-up.
it.skip('emits ACCESSES edges for field reads in chains', () => {
it('emits ACCESSES edges for field reads in chains', () => {
const accesses = getRelationships(result, 'ACCESSES');
const addressReads = accesses.filter(
(e) => e.target === 'address' && e.rel.reason === 'read',