diff --git a/gitnexus/CHANGELOG.md b/gitnexus/CHANGELOG.md index 67073782e..7b75b9ec0 100644 --- a/gitnexus/CHANGELOG.md +++ b/gitnexus/CHANGELOG.md @@ -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 diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 53f7b1e8c..4cc15ae69 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -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); diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index e8aa35c39..f4664ff76 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -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 = {}; diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index 02dfe5b3d..ffa5dfc36 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -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'). * Used by lookupRawReturnType for for-loop element extraction. */ importedRawReturnTypes?: ReadonlyMap; + /** 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>, diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 6c1183de9..002247ebf 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -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 = 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) { diff --git a/gitnexus/test/integration/resolvers/dart.test.ts b/gitnexus/test/integration/resolvers/dart.test.ts index 65a87d348..c00e752b2 100644 --- a/gitnexus/test/integration/resolvers/dart.test.ts +++ b/gitnexus/test/integration/resolvers/dart.test.ts @@ -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',