diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 0af27fdc5..4db61b4ad 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -20,15 +20,24 @@ import { extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, - MAX_CHAIN_DEPTH, - extractCallChain, + extractMixedChain, } from './utils.js'; import { buildTypeEnv } from './type-env.js'; import type { ConstructorBinding } from './type-env.js'; import { getTreeSitterBufferSize } from './constants.js'; import type { ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js'; import { callRouters } from './call-routing.js'; -import { extractReturnTypeName } from './type-extractors/shared.js'; +import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js'; + +// Stdlib methods that preserve the receiver's type identity. When TypeEnv already +// strips nullable wrappers (Option → User), these chain steps are no-ops +// for type resolution — the current type passes through unchanged. +const TYPE_PRESERVING_METHODS = new Set([ + 'unwrap', 'expect', 'unwrap_or', 'unwrap_or_default', 'unwrap_or_else', // Rust Option/Result + 'clone', 'to_owned', 'as_ref', 'as_mut', 'borrow', 'borrow_mut', // Rust clone/borrow + 'get', // Kotlin/Java Optional.get() + 'orElseThrow', // Java Optional +]); /** * Walk up the AST from a node to find the enclosing function/method. @@ -271,52 +280,56 @@ export const processCalls = async ( receiverTypeName = receiverName; } } - // Fall back to field-access resolution when the receiver is a member_expression - // (e.g. user.address.save() — the receiver of save() is user.address, a field access). + // Fall back to mixed chain resolution when the receiver is a complex expression + // (field chain, call chain, or interleaved — e.g. user.address.city.save() or + // svc.getUser().address.save()). Handles all cases with a single unified walk. if (callForm === 'member' && !receiverTypeName && !receiverName) { const receiverNode = extractReceiverNode(nameNode); - if (receiverNode && !CALL_EXPRESSION_TYPES.has(receiverNode.type)) { - const parts = extractMemberAccessParts(receiverNode); - if (parts) { - // Resolve the object's type from TypeEnv - let objectType = typeEnv ? typeEnv.lookup(parts.objectName, callNode) : undefined; - if (!objectType && verifiedReceivers.size > 0) { - const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx); - const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : ''; - objectType = lookupReceiverType(verifiedReceivers, funcName, parts.objectName); - } - if (objectType) { - receiverTypeName = resolveFieldAccessType(objectType, parts.propertyName, file.path, ctx); - } - } - } - } - // Fall back to chained call resolution when the receiver is a call expression - // (e.g. svc.getUser().save() — receiver of save() is getUser(), not a simple identifier). - if (callForm === 'member' && !receiverTypeName && !receiverName) { - const receiverNode = extractReceiverNode(nameNode); - if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) { - const extracted = extractCallChain(receiverNode); - if (extracted) { - // Resolve the base receiver type if possible - let baseType = extracted.baseReceiverName && typeEnv + if (receiverNode) { + const extracted = extractMixedChain(receiverNode); + if (extracted && extracted.chain.length > 0) { + let currentType = extracted.baseReceiverName && typeEnv ? typeEnv.lookup(extracted.baseReceiverName, callNode) : undefined; - if (!baseType && extracted.baseReceiverName && verifiedReceivers.size > 0) { + if (!currentType && extracted.baseReceiverName && verifiedReceivers.size > 0) { const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx); const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : ''; - baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName); + currentType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName); } - // Class-as-receiver for chain base (e.g. UserService.find_user().save()) - if (!baseType && extracted.baseReceiverName) { + if (!currentType && extracted.baseReceiverName) { const cr = ctx.resolve(extracted.baseReceiverName, file.path); if (cr?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum', )) { - baseType = extracted.baseReceiverName; + currentType = extracted.baseReceiverName; } } - receiverTypeName = resolveChainedReceiver(extracted.chain, baseType, file.path, ctx); + if (currentType) { + for (const step of extracted.chain) { + if (!currentType) break; + if (step.kind === 'field') { + currentType = resolveFieldAccessType(currentType, step.name, file.path, ctx); + } else { + const resolved = resolveCallTarget( + { calledName: step.name, callForm: 'member', receiverTypeName: currentType }, + file.path, + ctx, + ); + if (!resolved) { + // Stdlib passthrough: unwrap(), clone(), etc. preserve the receiver type + if (TYPE_PRESERVING_METHODS.has(step.name)) continue; + currentType = undefined; break; + } + const candidates = ctx.symbols.lookupFuzzy(step.name); + const symDef = candidates.find(c => c.nodeId === resolved.nodeId); + if (!symDef?.returnType) { currentType = undefined; break; } + const retType = extractReturnTypeName(symDef.returnType); + if (!retType) { currentType = undefined; break; } + currentType = retType; + } + } + receiverTypeName = currentType; + } } } } @@ -416,46 +429,6 @@ const toResolveResult = ( reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global', }); -/** - * Resolve a chain of intermediate method calls to find the receiver type for a - * final member call. Called when the receiver of a call is itself a call - * expression (e.g. `svc.getUser().save()`). - * - * @param chainNames Ordered list of method names from outermost to innermost - * intermediate call (e.g. ['getUser'] for `svc.getUser().save()`). - * @param baseReceiverTypeName The already-resolved type of the base receiver - * (e.g. 'UserService' for `svc`), or undefined. - * @param currentFile The file path for resolution context. - * @param ctx The resolution context for symbol lookup. - * @returns The type name of the final intermediate call's return type, or undefined - * if resolution fails at any step. - */ -function resolveChainedReceiver( - chainNames: string[], - baseReceiverTypeName: string | undefined, - currentFile: string, - ctx: ResolutionContext, -): string | undefined { - let currentType = baseReceiverTypeName; - for (const name of chainNames) { - const resolved = resolveCallTarget( - { calledName: name, callForm: 'member', receiverTypeName: currentType }, - currentFile, - ctx, - ); - if (!resolved) return undefined; - - const candidates = ctx.symbols.lookupFuzzy(name); - const symDef = candidates.find(c => c.nodeId === resolved.nodeId); - if (!symDef?.returnType) return undefined; - - const returnTypeName = extractReturnTypeName(symDef.returnType); - if (!returnTypeName) return undefined; - - currentType = returnTypeName; - } - return currentType; -} /** * Resolve a function call to its target node ID using priority strategy: @@ -593,16 +566,6 @@ const lookupReceiverType = ( return map.get(fileLevelKey); }; -/** - * Resolve a property/field access on a typed receiver to determine the field's declared type. - * Used when a call's receiver is a member_expression (e.g. `user.address.save()` — the receiver - * of `save()` is `user.address` which is a field access, not a method call). - * - * Walks up to MAX_CHAIN_DEPTH levels of nested member_expression nodes to handle chains - * like `user.address.city.getName()`. - * - * @returns The resolved type of the deepest field access, or undefined if resolution fails. - */ /** * Extract object and property names from a member-access AST node. * Handles cross-language AST variations: @@ -673,14 +636,17 @@ const resolveFieldAccessType = ( const typeResolved = ctx.resolve(receiverName, filePath); if (!typeResolved) return undefined; const classDef = typeResolved.candidates.find( - d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface', + d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface' + || d.type === 'Enum' || d.type === 'Record' || d.type === 'Impl', ); if (!classDef) return undefined; const fieldDef = ctx.symbols.lookupFieldByOwner(classDef.nodeId, fieldName); if (!fieldDef?.declaredType) return undefined; - return extractReturnTypeName(fieldDef.declaredType); + // Use stripNullable (not extractReturnTypeName) — field types like List + // should be preserved as-is, not unwrapped to User. Only strip nullable wrappers. + return stripNullable(fieldDef.declaredType); }; /** @@ -748,55 +714,52 @@ export const processCallsFromExtracted = async ( } } - // Step 1c: field-access resolution (e.g. user.address.save()) - // When the parse-worker captured a receiverFieldAccess, resolve the field's declared type. - if (!effectiveCall.receiverTypeName && effectiveCall.receiverFieldAccess) { - const { objectName, fieldName } = effectiveCall.receiverFieldAccess; - // Resolve the object's type from constructor bindings or class-as-receiver - let objectType: string | undefined; - if (receiverMap) { + // Step 1c: mixed chain resolution (field, call, or interleaved — e.g. svc.getUser().address.save()). + // Runs whenever receiverMixedChain is present. Steps 1/1b may have resolved the base receiver + // type already; that type is used as the chain's starting point. + if (effectiveCall.receiverMixedChain?.length) { + // Use the already-resolved base type (from Steps 1/1b) or look it up now. + let currentType: string | undefined = effectiveCall.receiverTypeName; + if (!currentType && effectiveCall.receiverName && receiverMap) { const callFuncName = extractFuncNameFromSourceId(effectiveCall.sourceId); - objectType = lookupReceiverType(receiverMap, callFuncName, objectName); + currentType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName); } - if (!objectType) { - const typeResolved = ctx.resolve(objectName, effectiveCall.filePath); + if (!currentType && effectiveCall.receiverName) { + const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath); if (typeResolved?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum', )) { - objectType = objectName; + currentType = effectiveCall.receiverName; } } - if (objectType) { - const fieldType = resolveFieldAccessType(objectType, fieldName, effectiveCall.filePath, ctx); - if (fieldType) { - effectiveCall = { ...effectiveCall, receiverTypeName: fieldType }; + if (currentType) { + for (const step of effectiveCall.receiverMixedChain) { + if (!currentType) break; + if (step.kind === 'field') { + currentType = resolveFieldAccessType(currentType, step.name, effectiveCall.filePath, ctx); + } else { + // step.kind === 'call': resolve the method and get its return type + const resolved = resolveCallTarget( + { calledName: step.name, callForm: 'member', receiverTypeName: currentType }, + effectiveCall.filePath, + ctx, + ); + if (!resolved) { + // Stdlib passthrough: unwrap(), clone(), etc. preserve the receiver type + if (TYPE_PRESERVING_METHODS.has(step.name)) continue; + currentType = undefined; break; + } + const candidates = ctx.symbols.lookupFuzzy(step.name); + const symDef = candidates.find(c => c.nodeId === resolved.nodeId); + if (!symDef?.returnType) { currentType = undefined; break; } + const returnTypeName = extractReturnTypeName(symDef.returnType); + if (!returnTypeName) { currentType = undefined; break; } + currentType = returnTypeName; + } + } + if (currentType) { + effectiveCall = { ...effectiveCall, receiverTypeName: currentType }; } - } - } - - // Step 2: if the call has a receiver call chain (e.g. svc.getUser().save()), - // resolve the chain to determine the final receiver type. - // This runs whenever receiverCallChain is present — even when Step 1 set a - // receiverTypeName, that type is the BASE receiver (e.g. UserService for svc), - // and the chain must be walked to produce the FINAL receiver (e.g. User from - // getUser() : User). - if (effectiveCall.receiverCallChain?.length) { - // Step 1 may have resolved the base receiver type (e.g. svc → UserService). - // Use it as the starting point for chain resolution. - let baseType = effectiveCall.receiverTypeName; - // If Step 1 didn't resolve it, try the receiver map directly. - if (!baseType && effectiveCall.receiverName && receiverMap) { - const callFuncName = extractFuncNameFromSourceId(effectiveCall.sourceId); - baseType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName); - } - const chainedType = resolveChainedReceiver( - effectiveCall.receiverCallChain, - baseType, - effectiveCall.filePath, - ctx, - ); - if (chainedType) { - effectiveCall = { ...effectiveCall, receiverTypeName: chainedType }; } } diff --git a/gitnexus/src/core/ingestion/call-routing.ts b/gitnexus/src/core/ingestion/call-routing.ts index 50a91d29b..4691fe4cb 100644 --- a/gitnexus/src/core/ingestion/call-routing.ts +++ b/gitnexus/src/core/ingestion/call-routing.ts @@ -65,6 +65,8 @@ export interface RubyPropertyItem { accessorType: RubyAccessorType; startLine: number; endLine: number; + /** YARD @return [Type] annotation preceding the attr_accessor call */ + declaredType?: string; } // ── Pre-allocated singletons for common return values ──────────────────────── @@ -129,6 +131,25 @@ export function routeRubyCall(calledName: string, callNode: any): RubyCallRoutin // ── attr_accessor / attr_reader / attr_writer → property definitions ─── if (calledName === 'attr_accessor' || calledName === 'attr_reader' || calledName === 'attr_writer') { + // Extract YARD @return [Type] from preceding comment (e.g. `# @return [Address]`) + let yardType: string | undefined; + let sibling = callNode.previousSibling; + while (sibling) { + if (sibling.type === 'comment') { + const match = /@return\s+\[([^\]]+)\]/.exec(sibling.text); + if (match) { + const raw = match[1].trim(); + // Extract simple type name: "User", "Array" → "User" + const simple = raw.match(/^([A-Z]\w*)/); + if (simple) yardType = simple[1]; + break; + } + } else if (sibling.isNamed) { + break; // stop at non-comment named sibling + } + sibling = sibling.previousSibling; + } + const items: RubyPropertyItem[] = []; const argList = callNode.childForFieldName?.('arguments'); for (const arg of (argList?.children ?? [])) { @@ -138,6 +159,7 @@ export function routeRubyCall(calledName: string, callNode: any): RubyCallRoutin accessorType: calledName as RubyAccessorType, startLine: arg.startPosition.row, endLine: arg.endPosition.row, + ...(yardType ? { declaredType: yardType } : {}), }); } } diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index 7bb648f86..f041d1440 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -5,11 +5,12 @@ import { LANGUAGE_QUERIES } from './tree-sitter-queries.js'; import { generateId } from '../../lib/utils.js'; import { SymbolTable } from './symbol-table.js'; import { ASTCache } from './ast-cache.js'; -import { getLanguageFromFilename, yieldToEventLoop, DEFINITION_CAPTURE_KEYS, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js'; -import { extractSimpleTypeName } from './type-extractors/shared.js'; +import { getLanguageFromFilename, yieldToEventLoop, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js'; +import { extractPropertyDeclaredType } from './type-extractors/shared.js'; import { isNodeExported } from './export-detection.js'; import { detectFrameworkFromAST } from './framework-detection.js'; import { typeConfigs } from './type-extractors/index.js'; +import { SupportedLanguages } from '../../config/supported-languages.js'; import { WorkerPool } from './workers/worker-pool.js'; import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js'; import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js'; @@ -202,7 +203,22 @@ const processParsingSequential = async ( let nodeLabel = 'CodeElement'; - if (captureMap['definition.function']) nodeLabel = 'Function'; + if (captureMap['definition.function']) { + // C/C++: @definition.function is broad and also matches inline class methods (inside + // a class/struct body). Those are already captured by @definition.method, so skip + // the duplicate Function entry to prevent double-indexing in globalIndex. + if (language === SupportedLanguages.CPlusPlus || language === SupportedLanguages.C) { + let ancestor = captureMap['definition.function']?.parent; + while (ancestor) { + if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') { + break; + } + ancestor = ancestor.parent; + } + if (ancestor) return; // inside a class body — handled by @definition.method + } + nodeLabel = 'Function'; + } else if (captureMap['definition.class']) nodeLabel = 'Class'; else if (captureMap['definition.interface']) nodeLabel = 'Interface'; else if (captureMap['definition.method']) nodeLabel = 'Method'; @@ -278,34 +294,9 @@ const processParsingSequential = async ( const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null; // Extract declared type for Property nodes (field/property type annotations) - let declaredType: string | undefined; - if (nodeLabel === 'Property' && definitionNode) { - const typeNode = definitionNode.childForFieldName?.('type'); - if (typeNode) { - declaredType = extractSimpleTypeName(typeNode) ?? typeNode.text?.trim(); - } - if (!declaredType) { - // TypeScript pattern: look for type_annotation child - for (let i = 0; i < definitionNode.childCount; i++) { - const child = definitionNode.child(i); - if (child?.type === 'type_annotation') { - for (let j = 0; j < child.childCount; j++) { - const typeChild = child.child(j); - if (typeChild && typeChild.type !== ':') { - declaredType = extractSimpleTypeName(typeChild) ?? typeChild.text?.trim(); - break; - } - } - break; - } - } - } - // Java: type is on the parent field_declaration - if (!declaredType && definitionNode.parent) { - const parentType = definitionNode.parent.childForFieldName?.('type'); - if (parentType) declaredType = extractSimpleTypeName(parentType) ?? undefined; - } - } + const declaredType = (nodeLabel === 'Property' && definitionNode) + ? extractPropertyDeclaredType(definitionNode) + : undefined; symbolTable.add(file.path, nodeName, nodeId, nodeLabel, { parameterCount: methodSig?.parameterCount, diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 9cdae7986..0b09f01e8 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -66,6 +66,15 @@ export const TYPESCRIPT_QUERIES = ` (public_field_definition name: (property_identifier) @name) @definition.property +; Private class fields: #address: Address +(public_field_definition + name: (private_property_identifier) @name) @definition.property + +; Constructor parameter properties: constructor(public address: Address) +(required_parameter + (accessibility_modifier) + pattern: (identifier) @name) @definition.property + ; Heritage queries - class extends (class_declaration name: (type_identifier) @heritage.class @@ -132,6 +141,10 @@ export const JAVASCRIPT_QUERIES = ` (new_expression constructor: (identifier) @call.name) @call +; Class fields — field_definition captures JS class fields (class User { address = ... }) +(field_definition + property: (property_identifier) @name) @definition.property + ; Heritage queries - class extends (JavaScript uses different AST than TypeScript) ; In tree-sitter-javascript, class_heritage directly contains the parent identifier (class_declaration @@ -164,6 +177,14 @@ export const PYTHON_QUERIES = ` function: (attribute attribute: (identifier) @call.name)) @call +; Class attribute type annotations — PEP 526: address: Address or address: Address = Address() +; Both bare annotations (address: Address) and annotated assignments (name: str = "test") +; are parsed as (assignment left: ... type: ...) in tree-sitter-python. +(expression_statement + (assignment + left: (identifier) @name + type: (type))) @definition.property + ; Heritage queries - Python class inheritance (class_definition name: (identifier) @heritage.class @@ -313,6 +334,11 @@ export const CPP_QUERIES = ` (declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function (declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function +; Class/struct data member fields (Address address; int count;) +; Uses field_identifier to exclude method declarations (which use function_declarator) +(field_declaration + declarator: (field_identifier) @name) @definition.property + ; Inline class method declarations (inside class body, no body: void Foo();) (field_declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.method @@ -428,6 +454,11 @@ export const RUST_QUERIES = ` ; Struct literal construction: User { name: value } (struct_expression name: (type_identifier) @call.name) @call +; Struct fields — named field declarations inside struct bodies +(field_declaration_list + (field_declaration + name: (field_identifier) @name) @definition.property) + ; Heritage (trait implementation) — all combinations of concrete/generic trait × concrete/generic type (impl_item trait: (type_identifier) @heritage.trait type: (type_identifier) @heritage.class) @heritage (impl_item trait: (generic_type type: (type_identifier) @heritage.trait) type: (type_identifier) @heritage.class) @heritage diff --git a/gitnexus/src/core/ingestion/type-extractors/rust.ts b/gitnexus/src/core/ingestion/type-extractors/rust.ts index 6bdc379b4..c3d389638 100644 --- a/gitnexus/src/core/ingestion/type-extractors/rust.ts +++ b/gitnexus/src/core/ingestion/type-extractors/rust.ts @@ -95,7 +95,7 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map, _classNames: ClassNameLookup): void => { +const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map, classNames: ClassNameLookup): void => { // Skip if there's an explicit type annotation — Tier 0 already handled it if (node.childForFieldName('type') !== null) return; const pattern = node.childForFieldName('pattern'); @@ -116,6 +116,13 @@ const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map { + if (!definitionNode) return undefined; + + // Strategy 1: Look for a `type` or `type_annotation` named field + const typeNode = definitionNode.childForFieldName?.('type'); + if (typeNode) { + const typeName = extractSimpleTypeName(typeNode); + if (typeName) return typeName; + // Fallback: use the raw text (for complex types like User[] or List) + const text = typeNode.text?.trim(); + if (text && text.length < 100) return text; + } + + // Strategy 2: Walk children looking for type_annotation (TypeScript pattern) + for (let i = 0; i < definitionNode.childCount; i++) { + const child = definitionNode.child(i); + if (!child) continue; + if (child.type === 'type_annotation') { + // Type annotation has the actual type as a child + for (let j = 0; j < child.childCount; j++) { + const typeChild = child.child(j); + if (typeChild && typeChild.type !== ':') { + const typeName = extractSimpleTypeName(typeChild); + if (typeName) return typeName; + const text = typeChild.text?.trim(); + if (text && text.length < 100) return text; + } + } + } + } + + // Strategy 3: For Java field_declaration, the type is a sibling of variable_declarator + // AST: (field_declaration type: (type_identifier) declarator: (variable_declarator ...)) + const parentDecl = definitionNode.parent; + if (parentDecl) { + const parentType = parentDecl.childForFieldName?.('type'); + if (parentType) { + const typeName = extractSimpleTypeName(parentType); + if (typeName) return typeName; + } + } + + // Strategy 4: Kotlin property_declaration — type is nested inside variable_declaration child + // AST: (property_declaration (variable_declaration name: ... type: (user_type ...))) + for (let i = 0; i < definitionNode.childCount; i++) { + const child = definitionNode.child(i); + if (child?.type === 'variable_declaration') { + const varType = child.childForFieldName?.('type'); + if (varType) { + const typeName = extractSimpleTypeName(varType); + if (typeName) return typeName; + const text = varType.text?.trim(); + if (text && text.length < 100) return text; + } + } + } + + // Strategy 5: PHP @var PHPDoc — look for preceding comment with @var Type + // Handles pre-PHP-7.4 code: /** @var Address */ public $address; + const prevSibling = definitionNode.previousNamedSibling ?? definitionNode.parent?.previousNamedSibling; + if (prevSibling?.type === 'comment') { + const commentText = prevSibling.text; + const varMatch = commentText?.match(/@var\s+([A-Z][\w\\]*)/); + if (varMatch) { + // Strip namespace prefix: \App\Models\User → User + const raw = varMatch[1]; + const base = raw.includes('\\') ? raw.split('\\').pop()! : raw; + if (base && /^[A-Z]\w*$/.test(base)) return base; + } + } + + return undefined; +}; diff --git a/gitnexus/src/core/ingestion/utils.ts b/gitnexus/src/core/ingestion/utils.ts index b1762d532..7cbb0e6a2 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -264,7 +264,7 @@ export const CLASS_CONTAINER_TYPES = new Set([ 'class_declaration', 'abstract_class_declaration', 'interface_declaration', 'struct_declaration', 'record_declaration', 'class_specifier', 'struct_specifier', - 'impl_item', 'trait_item', + 'impl_item', 'trait_item', 'struct_item', 'enum_item', 'class_definition', 'trait_declaration', 'protocol_declaration', @@ -286,6 +286,8 @@ export const CONTAINER_TYPE_TO_LABEL: Record = { class_definition: 'Class', impl_item: 'Impl', trait_item: 'Trait', + struct_item: 'Struct', + enum_item: 'Enum', trait_declaration: 'Trait', record_declaration: 'Record', protocol_declaration: 'Interface', @@ -1171,6 +1173,142 @@ export function extractCallChain( return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined; } +/** Node types representing member/field access across languages. */ +const FIELD_ACCESS_NODE_TYPES = new Set([ + 'member_expression', // TS/JS + 'member_access_expression', // C# + 'selector_expression', // Go + 'field_expression', // Rust/C++ + 'attribute', // Python + 'navigation_expression', // Kotlin/Swift + 'member_binding_expression', // C# null-conditional (user?.Address) +]); +/** One step in a mixed receiver chain. */ +export type MixedChainStep = { kind: 'field' | 'call'; name: string }; +/** + * Walk a receiver AST node that may interleave field accesses and method calls, + * building a unified chain of steps up to MAX_CHAIN_DEPTH. + * + * For `svc.getUser().address.save()`, called with the receiver of `save` + * (`svc.getUser().address`, a field access node): + * returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }], + * baseReceiverName: 'svc' } + * + * For `user.getAddress().city.getName()`, called with receiver of `getName` + * (`user.getAddress().city`): + * returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }], + * baseReceiverName: 'user' } + * + * Pure field chains and pure call chains are special cases (all steps same kind). + */ +export function extractMixedChain( + receiverNode: SyntaxNode, +): { chain: MixedChainStep[]; baseReceiverName: string | undefined } | undefined { + const chain: MixedChainStep[] = []; + let current: SyntaxNode = receiverNode; + while (chain.length < MAX_CHAIN_DEPTH) { + if (CALL_EXPRESSION_TYPES.has(current.type)) { + // ── Call expression: extract method name + inner receiver ──────────── + const funcNode = current.childForFieldName?.('function') + ?? current.childForFieldName?.('name') + ?? current.childForFieldName?.('method'); + let methodName: string | undefined; + let innerReceiver: SyntaxNode | null = null; + + if (funcNode) { + methodName = funcNode.lastNamedChild?.text ?? funcNode.text; + } + // Kotlin/Swift: call_expression → navigation_expression + if (!funcNode && current.type === 'call_expression') { + const callee = current.firstNamedChild; + if (callee?.type === 'navigation_expression') { + const suffix = callee.lastNamedChild; + if (suffix?.type === 'navigation_suffix') { + methodName = suffix.lastNamedChild?.text; + for (let i = 0; i < callee.namedChildCount; i++) { + const child = callee.namedChild(i); + if (child && child.type !== 'navigation_suffix') { innerReceiver = child; break; } + } + } + } + } + if (!methodName) break; + chain.unshift({ kind: 'call', name: methodName }); + + if (!innerReceiver && funcNode) { + innerReceiver = funcNode.childForFieldName?.('object') + ?? funcNode.childForFieldName?.('value') + ?? funcNode.childForFieldName?.('operand') + ?? funcNode.childForFieldName?.('argument') // C/C++ field_expression + ?? funcNode.childForFieldName?.('expression') + ?? null; + } + if (!innerReceiver && current.type === 'method_invocation') { + innerReceiver = current.childForFieldName?.('object') ?? null; + } + if (!innerReceiver && (current.type === 'member_call_expression' || current.type === 'nullsafe_member_call_expression')) { + innerReceiver = current.childForFieldName?.('object') ?? null; + } + if (!innerReceiver && current.type === 'call') { + innerReceiver = current.childForFieldName?.('receiver') ?? null; + } + if (!innerReceiver) break; + + if (CALL_EXPRESSION_TYPES.has(innerReceiver.type) || FIELD_ACCESS_NODE_TYPES.has(innerReceiver.type)) { + current = innerReceiver; + } else { + return { chain, baseReceiverName: innerReceiver.text || undefined }; + } + } else if (FIELD_ACCESS_NODE_TYPES.has(current.type)) { + // ── Field/member access: extract property name + inner object ───────── + let propertyName: string | undefined; + let innerObject: SyntaxNode | null = null; + + if (current.type === 'navigation_expression') { + for (const child of current.children ?? []) { + if (child.type === 'navigation_suffix') { + for (const sc of child.children ?? []) { + if (sc.isNamed && sc.type !== '.') { propertyName = sc.text; break; } + } + } else if (child.isNamed && !innerObject) { + innerObject = child; + } + } + } else if (current.type === 'attribute') { + innerObject = current.childForFieldName?.('object') ?? null; + propertyName = current.childForFieldName?.('attribute')?.text; + } else { + innerObject = current.childForFieldName?.('object') + ?? current.childForFieldName?.('value') + ?? current.childForFieldName?.('operand') + ?? current.childForFieldName?.('argument') // C/C++ field_expression + ?? current.childForFieldName?.('expression') + ?? null; + propertyName = (current.childForFieldName?.('property') + ?? current.childForFieldName?.('field') + ?? current.childForFieldName?.('name'))?.text; + } + + if (!propertyName) break; + chain.unshift({ kind: 'field', name: propertyName }); + + if (!innerObject) break; + + if (CALL_EXPRESSION_TYPES.has(innerObject.type) || FIELD_ACCESS_NODE_TYPES.has(innerObject.type)) { + current = innerObject; + } else { + return { chain, baseReceiverName: innerObject.text || undefined }; + } + } else { + // Simple identifier — this is the base receiver + return chain.length > 0 + ? { chain, baseReceiverName: current.text || undefined } + : undefined; + } + } + + return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined; +} diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 7d16b1cc3..b1ff3077e 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -36,8 +36,8 @@ import { inferCallForm, extractReceiverName, extractReceiverNode, - CALL_EXPRESSION_TYPES, - extractCallChain, + extractMixedChain, + type MixedChainStep, } from '../utils.js'; import { buildTypeEnv } from '../type-env.js'; import type { ConstructorBinding } from '../type-env.js'; @@ -48,66 +48,7 @@ import { generateId } from '../../../lib/utils.js'; import { extractNamedBindings } from '../named-binding-extraction.js'; import { appendKotlinWildcard } from '../resolvers/index.js'; import { callRouters } from '../call-routing.js'; -import { extractSimpleTypeName } from '../type-extractors/shared.js'; - -// ============================================================================ -// Property type extraction -// ============================================================================ - -/** - * Extract the declared type from a field/property AST definition node. - * Handles common patterns across languages: - * - TypeScript: `name: Type` → type_annotation child - * - Java: `Type name` → type child on field_declaration - * - C#: `Type Name { get; set; }` → type child on property_declaration - * - Go: `Name Type` → type child on field_declaration - * - * Returns the normalized type name, or undefined if no type can be extracted. - */ -const extractPropertyDeclaredType = (definitionNode: any): string | undefined => { - if (!definitionNode) return undefined; - - // Strategy 1: Look for a `type` or `type_annotation` named field - const typeNode = definitionNode.childForFieldName?.('type'); - if (typeNode) { - const typeName = extractSimpleTypeName(typeNode); - if (typeName) return typeName; - // Fallback: use the raw text (for complex types like User[] or List) - const text = typeNode.text?.trim(); - if (text && text.length < 100) return text; - } - - // Strategy 2: Walk children looking for type_annotation (TypeScript pattern) - for (let i = 0; i < definitionNode.childCount; i++) { - const child = definitionNode.child(i); - if (!child) continue; - if (child.type === 'type_annotation') { - // Type annotation has the actual type as a child - for (let j = 0; j < child.childCount; j++) { - const typeChild = child.child(j); - if (typeChild && typeChild.type !== ':') { - const typeName = extractSimpleTypeName(typeChild); - if (typeName) return typeName; - const text = typeChild.text?.trim(); - if (text && text.length < 100) return text; - } - } - } - } - - // Strategy 3: For Java field_declaration, the type is a sibling of variable_declarator - // AST: (field_declaration type: (type_identifier) declarator: (variable_declarator ...)) - const parentDecl = definitionNode.parent; - if (parentDecl) { - const parentType = parentDecl.childForFieldName?.('type'); - if (parentType) { - const typeName = extractSimpleTypeName(parentType); - if (typeName) return typeName; - } - } - - return undefined; -}; +import { extractPropertyDeclaredType } from '../type-extractors/shared.js'; // ============================================================================ // Types for serializable results @@ -172,19 +113,14 @@ export interface ExtractedCall { /** Resolved type name of the receiver (e.g., 'User' for user.save() when user: User) */ receiverTypeName?: string; /** - * Chained call names when the receiver is itself a call expression. - * For `svc.getUser().save()`, the `save` ExtractedCall gets receiverCallChain = ['getUser'] - * with receiverName = 'svc'. The chain is ordered outermost-last, e.g.: - * `a.b().c().d()` → calledName='d', receiverCallChain=['b','c'], receiverName='a' + * Unified mixed chain when the receiver is a chain of field accesses and/or method calls. + * Steps are ordered base-first (innermost to outermost). Examples: + * `svc.getUser().save()` → chain=[{kind:'call',name:'getUser'}], receiverName='svc' + * `user.address.save()` → chain=[{kind:'field',name:'address'}], receiverName='user' + * `svc.getUser().address.save()` → chain=[{kind:'call',name:'getUser'},{kind:'field',name:'address'}] * Length is capped at MAX_CHAIN_DEPTH (3). */ - receiverCallChain?: string[]; - /** - * Field-access receiver when the receiver is a member_expression (not a call or identifier). - * For `user.address.save()`, the `save` ExtractedCall gets - * receiverFieldAccess = { objectName: 'user', fieldName: 'address' }. - */ - receiverFieldAccess?: { objectName: string; fieldName: string }; + receiverMixedChain?: MixedChainStep[]; } export interface ExtractedHeritage { @@ -1032,6 +968,7 @@ const processFileGroup = ( nodeId, type: 'Property', ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}), + ...(item.declaredType ? { declaredType: item.declaredType } : {}), }); const fileId = generateId('File', file.path); const relId = generateId('DEFINES', `${fileId}->${nodeId}`); @@ -1067,72 +1004,24 @@ const processFileGroup = ( const callForm = inferCallForm(callNode, callNameNode); let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined; let receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined; - let receiverCallChain: string[] | undefined; - - // When the receiver is a call_expression (e.g. svc.getUser().save()), - // extractReceiverName returns undefined because it refuses complex expressions. - // Instead, walk the receiver node to build a call chain for deferred resolution. - // We capture the base receiver name so processCallsFromExtracted can look it up - // from constructor bindings. receiverTypeName is intentionally left unset here — - // the chain resolver in processCallsFromExtracted needs the base type as input and - // produces the final receiver type as output. - let receiverFieldAccess: { objectName: string; fieldName: string } | undefined; + let receiverMixedChain: MixedChainStep[] | undefined; + // When the receiver is a complex expression (call chain, field chain, or mixed), + // extractReceiverName returns undefined. Walk the receiver node to build a unified + // mixed chain for deferred resolution in processCallsFromExtracted. if (callForm === 'member' && receiverName === undefined && !receiverTypeName) { const receiverNode = extractReceiverNode(callNameNode); - if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) { - const extracted = extractCallChain(receiverNode); - if (extracted) { - receiverCallChain = extracted.chain; - // Set receiverName to the base object so Step 1 in processCallsFromExtracted - // can resolve it via constructor bindings to a base type for the chain. + if (receiverNode) { + const extracted = extractMixedChain(receiverNode); + if (extracted && extracted.chain.length > 0) { + receiverMixedChain = extracted.chain; receiverName = extracted.baseReceiverName; - // Also try the type environment immediately (covers explicitly-typed locals - // and annotated parameters like `fn process(svc: &UserService)`). - // This sets a base type that chain resolution (Step 2) will use as input. + // Try the type environment immediately for the base receiver + // (covers explicitly-typed locals and annotated parameters). if (receiverName) { receiverTypeName = typeEnv.lookup(receiverName, callNode); } } - } else if (receiverNode) { - // Receiver is a member_expression (field access like user.address.save()). - // Extract object and property so processCallsFromExtracted can resolve the field type. - let objectName: string | undefined; - let fieldName: string | undefined; - - // Kotlin/Swift: navigation_expression — object is first child, property inside navigation_suffix - if (receiverNode.type === 'navigation_expression') { - for (const child of receiverNode.children ?? []) { - if (child.type === 'navigation_suffix') { - for (const sc of child.children ?? []) { - if (sc.isNamed && sc.type !== '.') { fieldName = sc.text; break; } - } - } else if (child.isNamed && !objectName) { - objectName = child.text; - } - } - } else { - // General: try standard field names used across grammars - const objectNode = receiverNode.childForFieldName?.('object') - ?? receiverNode.childForFieldName?.('value') - ?? receiverNode.childForFieldName?.('operand') - ?? receiverNode.childForFieldName?.('expression'); - const propertyNode = receiverNode.childForFieldName?.('property') - ?? receiverNode.childForFieldName?.('field') - ?? receiverNode.childForFieldName?.('name'); - if (objectNode) objectName = objectNode.text; - if (propertyNode) fieldName = propertyNode.text; - } - - if (objectName && fieldName) { - receiverFieldAccess = { objectName, fieldName }; - // Try resolving the object's type immediately from TypeEnv - const objectType = typeEnv.lookup(objectName, callNode); - if (objectType) { - receiverName = objectName; - receiverTypeName = objectType; - } - } } } @@ -1144,8 +1033,7 @@ const processFileGroup = ( ...(callForm !== undefined ? { callForm } : {}), ...(receiverName !== undefined ? { receiverName } : {}), ...(receiverTypeName !== undefined ? { receiverTypeName } : {}), - ...(receiverCallChain !== undefined ? { receiverCallChain } : {}), - ...(receiverFieldAccess !== undefined ? { receiverFieldAccess } : {}), + ...(receiverMixedChain !== undefined ? { receiverMixedChain } : {}), }); } } @@ -1195,6 +1083,23 @@ const processFileGroup = ( const nodeLabel = getLabelFromCaptures(captureMap); if (!nodeLabel) continue; + // C/C++: @definition.function is broad and also matches inline class methods (inside + // a class/struct body). Those are already captured by @definition.method, so skip + // the duplicate Function entry to prevent double-indexing in globalIndex. + if ( + (language === SupportedLanguages.CPlusPlus || language === SupportedLanguages.C) && + nodeLabel === 'Function' + ) { + let ancestor = captureMap['definition.function']?.parent; + while (ancestor) { + if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') { + break; // inside a class body — duplicate of @definition.method + } + ancestor = ancestor.parent; + } + if (ancestor) continue; // found a class/struct ancestor → skip + } + const nameNode = captureMap['name']; // Synthesize name for constructors without explicit @name capture (e.g. Swift init) if (!nameNode && nodeLabel !== 'Constructor') continue; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-chain-call/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-chain-call/app.cpp index 8493229fa..af1cd1bc3 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-chain-call/app.cpp +++ b/gitnexus/test/fixtures/lang-resolution/cpp-chain-call/app.cpp @@ -1,4 +1,5 @@ #include "service.h" +#include "user.h" #include "repo.h" void processUser() { diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/models.h b/gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/models.h new file mode 100644 index 000000000..6c0553b82 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/models.h @@ -0,0 +1,30 @@ +#pragma once + +class City { +public: + std::string zipCode; + + std::string getName() { + return "city"; + } +}; + +class Address { +public: + City city; + std::string street; + + void save() { + // persist address + } +}; + +class User { +public: + std::string name; + Address address; + + std::string greet() { + return name; + } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/service.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/service.cpp new file mode 100644 index 000000000..bfa5a904c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/service.cpp @@ -0,0 +1,9 @@ +#include "models.h" + +void processUser(User user) { + // 2-level chain: user.address → Address, then .save() → Address#save + user.address.save(); + + // 3-level chain: user.address → Address, .city → City, .getName() → City#getName + user.address.city.getName(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-field-types/models.h b/gitnexus/test/fixtures/lang-resolution/cpp-field-types/models.h new file mode 100644 index 000000000..0b91ffd26 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-field-types/models.h @@ -0,0 +1,20 @@ +#pragma once + +class Address { +public: + std::string city; + + void save() { + // persist address + } +}; + +class User { +public: + std::string name; + Address address; + + std::string greet() { + return name; + } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-field-types/service.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-field-types/service.cpp new file mode 100644 index 000000000..4b90c656c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-field-types/service.cpp @@ -0,0 +1,6 @@ +#include "models.h" + +void processUser(User user) { + // Field-access chain: user.address → Address, then .save() → Address#save + user.address.save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Models.cs b/gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Models.cs new file mode 100644 index 000000000..7e3b1ae00 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Models.cs @@ -0,0 +1,33 @@ +namespace DeepFieldChain; + +public class City +{ + public string ZipCode { get; set; } + + public string GetName() + { + return "city"; + } +} + +public class Address +{ + public City City { get; set; } + public string Street { get; set; } + + public void Save() + { + // persist address + } +} + +public class User +{ + public string Name { get; set; } + public Address Address { get; set; } + + public string Greet() + { + return Name; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Service.cs b/gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Service.cs new file mode 100644 index 000000000..52e8594bc --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Service.cs @@ -0,0 +1,13 @@ +namespace DeepFieldChain; + +public class Service +{ + public static void ProcessUser(User user) + { + // 2-level chain: user.Address → Address, then .Save() → Address#Save + user.Address.Save(); + + // 3-level chain: user.Address → Address, .City → City, .GetName() → City#GetName + user.Address.City.GetName(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/cmd/main.go b/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/cmd/main.go new file mode 100644 index 000000000..c848c9368 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/cmd/main.go @@ -0,0 +1,11 @@ +package main + +import "example.com/go-deep-field-chain/models" + +func processUser(user models.User) { + // 2-level chain: user.Address → Address, then .Save() → Address#Save + user.Address.Save() + + // 3-level chain: user.Address → Address, .City → City, .GetName() → City#GetName + user.Address.City.GetName() +} diff --git a/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/go.mod b/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/go.mod new file mode 100644 index 000000000..f9f8e3511 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/go.mod @@ -0,0 +1,3 @@ +module example.com/go-deep-field-chain + +go 1.21 diff --git a/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/models/models.go b/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/models/models.go new file mode 100644 index 000000000..ac615e385 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/models/models.go @@ -0,0 +1,27 @@ +package models + +type City struct { + ZipCode string +} + +func (c *City) GetName() string { + return "city" +} + +type Address struct { + City City + Street string +} + +func (a *Address) Save() bool { + return true +} + +type User struct { + Name string + Address Address +} + +func (u *User) Greet() string { + return u.Name +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/App.java b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/App.java new file mode 100644 index 000000000..a158e6168 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/App.java @@ -0,0 +1,11 @@ +import models.User; + +public class App { + public static void processUser(User user) { + // 2-level chain: user.address → Address, then .save() → Address#save + user.address.save(); + + // 3-level chain: user.address → Address, .city → City, .getName() → City#getName + user.address.city.getName(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/Address.java b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/Address.java new file mode 100644 index 000000000..c89d8132c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/Address.java @@ -0,0 +1,10 @@ +package models; + +public class Address { + public City city; + public String street; + + public void save() { + // persist address + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/City.java b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/City.java new file mode 100644 index 000000000..c843c5ccc --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/City.java @@ -0,0 +1,9 @@ +package models; + +public class City { + public String zipCode; + + public String getName() { + return "city"; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/User.java b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/User.java new file mode 100644 index 000000000..dc3b7e76a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/User.java @@ -0,0 +1,10 @@ +package models; + +public class User { + public String name; + public Address address; + + public String greet() { + return this.name; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/js-field-types/models.js b/gitnexus/test/fixtures/lang-resolution/js-field-types/models.js new file mode 100644 index 000000000..4d9d45fa4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-field-types/models.js @@ -0,0 +1,26 @@ +class Address { + city = ''; + + save() { + // persist address + } +} + +class User { + name = ''; + address = new Address(); + + greet() { + return this.name; + } +} + +class Config { + static DEFAULT = new Config(); + + validate() { + return true; + } +} + +module.exports = { Address, User, Config }; diff --git a/gitnexus/test/fixtures/lang-resolution/js-field-types/service.js b/gitnexus/test/fixtures/lang-resolution/js-field-types/service.js new file mode 100644 index 000000000..9f9494621 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/js-field-types/service.js @@ -0,0 +1,9 @@ +const { User, Config } = require('./models'); + +function processUser(user) { + user.address.save(); +} + +function validateConfig() { + Config.DEFAULT.validate(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Models.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Models.kt new file mode 100644 index 000000000..44e658a3b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Models.kt @@ -0,0 +1,25 @@ +class City { + var zipCode: String = "" + + fun getName(): String { + return "city" + } +} + +class Address { + var city: City = City() + var street: String = "" + + fun save() { + // persist address + } +} + +class User { + var name: String = "" + var address: Address = Address() + + fun greet(): String { + return name + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Service.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Service.kt new file mode 100644 index 000000000..b07cdc865 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Service.kt @@ -0,0 +1,7 @@ +fun processUser(user: User) { + // 2-level chain: user.address → Address, then .save() → Address#save + user.address.save() + + // 3-level chain: user.address → Address, .city → City, .getName() → City#getName + user.address.city.getName() +} diff --git a/gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Models.php b/gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Models.php new file mode 100644 index 000000000..1a91ced7a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Models.php @@ -0,0 +1,34 @@ +name; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Service.php b/gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Service.php new file mode 100644 index 000000000..05cd4b017 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Service.php @@ -0,0 +1,11 @@ +address → Address, then ->save() → Address#save + $user->address->save(); + + // 3-level chain: $user->address → Address, ->city → City, ->getName() → City#getName + $user->address->city->getName(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/python-field-types/models.py b/gitnexus/test/fixtures/lang-resolution/python-field-types/models.py new file mode 100644 index 000000000..fccf42ae2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-field-types/models.py @@ -0,0 +1,12 @@ +class Address: + city: str + + def save(self): + pass + +class User: + name: str + address: Address + + def greet(self) -> str: + return self.name diff --git a/gitnexus/test/fixtures/lang-resolution/python-field-types/service.py b/gitnexus/test/fixtures/lang-resolution/python-field-types/service.py new file mode 100644 index 000000000..f7c2a8d74 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/python-field-types/service.py @@ -0,0 +1,4 @@ +from models import User + +def process_user(user: User): + user.address.save() diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-field-types/models.rb b/gitnexus/test/fixtures/lang-resolution/ruby-field-types/models.rb new file mode 100644 index 000000000..a4bd432a7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-field-types/models.rb @@ -0,0 +1,20 @@ +class Address + # @return [String] + attr_accessor :city + + def save + true + end +end + +class User + # @return [String] + attr_accessor :name + + # @return [Address] + attr_accessor :address + + def greet + name + end +end diff --git a/gitnexus/test/fixtures/lang-resolution/ruby-field-types/service.rb b/gitnexus/test/fixtures/lang-resolution/ruby-field-types/service.rb new file mode 100644 index 000000000..a7da3e692 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ruby-field-types/service.rb @@ -0,0 +1,7 @@ +require_relative 'models' + +# @param user [User] +def process_user(user) + # Field-access chain: user.address → Address, then .save → Address#save + user.address.save +end diff --git a/gitnexus/test/fixtures/lang-resolution/rust-field-types/models.rs b/gitnexus/test/fixtures/lang-resolution/rust-field-types/models.rs new file mode 100644 index 000000000..3d9f571ff --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-field-types/models.rs @@ -0,0 +1,20 @@ +pub struct Address { + pub city: String, +} + +impl Address { + pub fn save(&self) { + // persist address + } +} + +pub struct User { + pub name: String, + pub address: Address, +} + +impl User { + pub fn greet(&self) -> &str { + &self.name + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-field-types/service.rs b/gitnexus/test/fixtures/lang-resolution/rust-field-types/service.rs new file mode 100644 index 000000000..be2ae66f7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-field-types/service.rs @@ -0,0 +1,5 @@ +use crate::models::{User, Address}; + +fn process_user(user: &User) { + user.address.save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/models.ts b/gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/models.ts new file mode 100644 index 000000000..b28b0a6ee --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/models.ts @@ -0,0 +1,25 @@ +export class City { + zipCode: string; + + getName(): string { + return 'city'; + } +} + +export class Address { + city: City; + street: string; + + save(): void { + // persist address + } +} + +export class User { + name: string; + address: Address; + + greet(): string { + return this.name; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/service.ts b/gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/service.ts new file mode 100644 index 000000000..bf561827d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/service.ts @@ -0,0 +1,9 @@ +import { User } from './models'; + +function processUser(user: User) { + // 2-level chain: user.address → Address, then .save() → Address#save + user.address.save(); + + // 3-level chain: user.address → Address, .city → City, .getName() → City#getName + user.address.city.getName(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/address.ts b/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/address.ts new file mode 100644 index 000000000..68559457c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/address.ts @@ -0,0 +1,7 @@ +export class Address { + city: string; + + save(): void { + // persist address + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/service.ts b/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/service.ts new file mode 100644 index 000000000..4c78a3fa2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/service.ts @@ -0,0 +1,7 @@ +import { User } from './user'; + +function processUser(user: User) { + // Field-access chain: user.address resolves to Address, then .save() must resolve + // to Address#save (NOT User#save) — only lookupFieldByOwner can disambiguate. + user.address.save(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/user.ts b/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/user.ts new file mode 100644 index 000000000..6386ecbf7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/user.ts @@ -0,0 +1,10 @@ +import { Address } from './address'; + +export class User { + name: string; + address: Address; + + save(): void { + // persist user + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/models.ts b/gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/models.ts new file mode 100644 index 000000000..baeb43deb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/models.ts @@ -0,0 +1,27 @@ +export class City { + getName(): string { + return 'city'; + } +} + +export class Address { + city: City; + + save(): void { + // persist address + } +} + +export class User { + address: Address; + + getAddress(): Address { + return this.address; + } +} + +export class UserService { + getUser(): User { + return new User(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/service.ts b/gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/service.ts new file mode 100644 index 000000000..227d87fbb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/service.ts @@ -0,0 +1,11 @@ +import { User, UserService } from './models'; + +function processWithService(svc: UserService) { + // call → field → call: svc.getUser().address.save() + svc.getUser().address.save(); +} + +function processWithUser(user: User) { + // field → call → call: user.getAddress().city.getName() + user.getAddress().city.getName(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/models.ts b/gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/models.ts new file mode 100644 index 000000000..513af1e9f --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/models.ts @@ -0,0 +1,22 @@ +export class Address { + city: string; + + save(): void { + // persist address + } +} + +export class User { + #secret: string; + + constructor( + public name: string, + public address: Address, + ) { + this.#secret = 'hidden'; + } + + greet(): string { + return this.name; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/service.ts b/gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/service.ts new file mode 100644 index 000000000..e11269c12 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/service.ts @@ -0,0 +1,5 @@ +import { User } from './models'; + +function processUser(user: User) { + user.address.save(); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 23aca2322..38ae3f989 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -812,3 +812,92 @@ describe('C++ pointer dereference in range-for', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (C++)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']); + }); + + it('detects Property nodes for C++ data member fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking fields to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(2); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); + + it('resolves user.address.save() → Address#save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save'); + const addressSave = saveCalls.find( + e => e.source === 'processUser' && e.targetFilePath.includes('models'), + ); + expect(addressSave).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (C++)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, City, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'User']); + }); + + it('detects Property nodes for all typed fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('city'); + expect(properties).toContain('zipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('Address → city'); + expect(edgeSet(propEdges)).toContain('City → zipCode'); + }); + + it('resolves 2-level chain: user.address.save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processUser'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('models')); + expect(addressSave).toBeDefined(); + }); + + it('resolves 3-level chain: user.address.city.getName() → City#getName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'getName' && e.source === 'processUser'); + const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('models')); + expect(cityGetName).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 2275e908c..1fbabd089 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -1193,3 +1193,93 @@ describe('C# nested member access foreach (this.data.Values)', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (C#)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'csharp-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, Service, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']); + }); + + it('detects Property nodes for C# properties', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('Address'); + expect(properties).toContain('Name'); + expect(properties).toContain('City'); + }); + + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → Address'); + expect(edgeSet(propEdges)).toContain('User → Name'); + expect(edgeSet(propEdges)).toContain('Address → City'); + }); + + it('resolves user.Address.Save() → Address#Save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'Save'); + const addressSave = saveCalls.find( + e => e.source === 'ProcessUser' && e.targetFilePath.includes('Models'), + ); + expect(addressSave).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (C#)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'csharp-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, City, Service, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'Service', 'User']); + }); + + it('detects Property nodes for C# properties', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('Address'); + expect(properties).toContain('City'); + expect(properties).toContain('ZipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toContain('User → Address'); + expect(edgeSet(propEdges)).toContain('Address → City'); + expect(edgeSet(propEdges)).toContain('City → ZipCode'); + }); + + it('resolves 2-level chain: user.Address.Save() → Address#Save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'Save' && e.source === 'ProcessUser'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('Models')); + expect(addressSave).toBeDefined(); + }); + + it('resolves 3-level chain: user.Address.City.GetName() → City#GetName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'GetName' && e.source === 'ProcessUser'); + const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('Models')); + expect(cityGetName).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/field-types.test.ts b/gitnexus/test/integration/resolvers/field-types.test.ts deleted file mode 100644 index beb4a638f..000000000 --- a/gitnexus/test/integration/resolvers/field-types.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Phase 8: Field/property type resolution — verifies that chained member access - * through typed fields resolves correctly (e.g. user.address.save() → Address#save). - * - * Per-language fixtures test: - * 1. Property nodes are extracted with correct ownerId linkage - * 2. HAS_PROPERTY edges link properties to their owning classes - * 3. Field-access chain resolution resolves user.address.save() → Address#save - */ -import { describe, it, expect, beforeAll } from 'vitest'; -import path from 'path'; -import { - FIXTURES, getRelationships, getNodesByLabel, edgeSet, - runPipelineFromRepo, type PipelineResult, -} from './helpers.js'; - -// --------------------------------------------------------------------------- -// TypeScript -// --------------------------------------------------------------------------- - -describe('Field type resolution (TypeScript)', () => { - let result: PipelineResult; - - beforeAll(async () => { - result = await runPipelineFromRepo( - path.join(FIXTURES, 'field-types'), - () => {}, - ); - }, 60000); - - it('detects classes: Address, Config, User', () => { - expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Config', 'User']); - }); - - it('detects Property nodes for typed fields', () => { - const properties = getNodesByLabel(result, 'Property'); - expect(properties).toContain('address'); - expect(properties).toContain('name'); - expect(properties).toContain('city'); - }); - - it('emits HAS_PROPERTY edges linking properties to classes', () => { - const propEdges = getRelationships(result, 'HAS_PROPERTY'); - expect(propEdges.length).toBeGreaterThanOrEqual(3); - expect(edgeSet(propEdges)).toContain('User → address'); - expect(edgeSet(propEdges)).toContain('User → name'); - expect(edgeSet(propEdges)).toContain('Address → city'); - }); - - it('resolves user.address.save() → Address#save via field type', () => { - const calls = getRelationships(result, 'CALLS'); - const saveCalls = calls.filter(e => e.target === 'save'); - const addressSave = saveCalls.find(e => e.targetFilePath.includes('models')); - expect(addressSave).toBeDefined(); - expect(addressSave!.source).toBe('processUser'); - }); -}); - -// --------------------------------------------------------------------------- -// Java -// --------------------------------------------------------------------------- - -describe('Field type resolution (Java)', () => { - let result: PipelineResult; - - beforeAll(async () => { - result = await runPipelineFromRepo( - path.join(FIXTURES, 'java-field-types'), - () => {}, - ); - }, 60000); - - it('detects classes: Address, App, User', () => { - expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'App', 'User']); - }); - - it('detects Property nodes for Java fields', () => { - const properties = getNodesByLabel(result, 'Property'); - expect(properties).toContain('address'); - expect(properties).toContain('name'); - expect(properties).toContain('city'); - }); - - it('emits HAS_PROPERTY edges linking properties to classes', () => { - const propEdges = getRelationships(result, 'HAS_PROPERTY'); - expect(propEdges.length).toBeGreaterThanOrEqual(3); - expect(edgeSet(propEdges)).toContain('User → address'); - expect(edgeSet(propEdges)).toContain('User → name'); - expect(edgeSet(propEdges)).toContain('Address → city'); - }); - - it('resolves user.address.save() → Address#save via field type', () => { - const calls = getRelationships(result, 'CALLS'); - const saveCalls = calls.filter(e => e.target === 'save'); - const addressSave = saveCalls.find( - e => e.source === 'processUser' && e.targetFilePath.includes('Address'), - ); - expect(addressSave).toBeDefined(); - }); -}); - -// --------------------------------------------------------------------------- -// C# -// --------------------------------------------------------------------------- - -describe('Field type resolution (C#)', () => { - let result: PipelineResult; - - beforeAll(async () => { - result = await runPipelineFromRepo( - path.join(FIXTURES, 'csharp-field-types'), - () => {}, - ); - }, 60000); - - it('detects classes: Address, Service, User', () => { - expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']); - }); - - it('detects Property nodes for C# properties', () => { - const properties = getNodesByLabel(result, 'Property'); - // C# property_declaration already captured before Phase 8 - expect(properties).toContain('Address'); - expect(properties).toContain('Name'); - expect(properties).toContain('City'); - }); - - it('emits HAS_PROPERTY edges linking properties to classes', () => { - const propEdges = getRelationships(result, 'HAS_PROPERTY'); - expect(propEdges.length).toBeGreaterThanOrEqual(3); - expect(edgeSet(propEdges)).toContain('User → Address'); - expect(edgeSet(propEdges)).toContain('User → Name'); - expect(edgeSet(propEdges)).toContain('Address → City'); - }); - - it('resolves user.Address.Save() → Address#Save via field type', () => { - const calls = getRelationships(result, 'CALLS'); - const saveCalls = calls.filter(e => e.target === 'Save'); - const addressSave = saveCalls.find( - e => e.source === 'ProcessUser' && e.targetFilePath.includes('Models'), - ); - expect(addressSave).toBeDefined(); - }); -}); - -// --------------------------------------------------------------------------- -// Go -// --------------------------------------------------------------------------- - -describe('Field type resolution (Go)', () => { - let result: PipelineResult; - - beforeAll(async () => { - result = await runPipelineFromRepo( - path.join(FIXTURES, 'go-field-types'), - () => {}, - ); - }, 60000); - - it('detects structs: Address, User', () => { - expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'User']); - }); - - it('detects Property nodes for Go struct fields', () => { - const properties = getNodesByLabel(result, 'Property'); - expect(properties).toContain('Address'); - expect(properties).toContain('Name'); - expect(properties).toContain('City'); - }); - - it('emits HAS_PROPERTY edges linking struct fields to structs', () => { - const propEdges = getRelationships(result, 'HAS_PROPERTY'); - expect(propEdges.length).toBeGreaterThanOrEqual(2); - }); - - it('resolves user.Address.Save() → Address#Save via field type', () => { - const calls = getRelationships(result, 'CALLS'); - const saveCalls = calls.filter(e => e.target === 'Save'); - const addressSave = saveCalls.find( - e => e.source === 'processUser' && e.targetFilePath.includes('models'), - ); - expect(addressSave).toBeDefined(); - }); -}); - -// --------------------------------------------------------------------------- -// Kotlin -// --------------------------------------------------------------------------- - -describe('Field type resolution (Kotlin)', () => { - let result: PipelineResult; - - beforeAll(async () => { - result = await runPipelineFromRepo( - path.join(FIXTURES, 'kotlin-field-types'), - () => {}, - ); - }, 60000); - - it('detects classes: Address, User', () => { - expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']); - }); - - it('detects Property nodes for Kotlin properties', () => { - const properties = getNodesByLabel(result, 'Property'); - // Kotlin property_declaration was already captured pre-Phase 8 - expect(properties).toContain('address'); - expect(properties).toContain('name'); - expect(properties).toContain('city'); - }); - - it('emits HAS_PROPERTY edges linking properties to classes', () => { - const propEdges = getRelationships(result, 'HAS_PROPERTY'); - expect(propEdges.length).toBeGreaterThanOrEqual(3); - expect(edgeSet(propEdges)).toContain('User → address'); - expect(edgeSet(propEdges)).toContain('User → name'); - expect(edgeSet(propEdges)).toContain('Address → city'); - }); - - it('resolves user.address.save() → Address#save via field type', () => { - const calls = getRelationships(result, 'CALLS'); - const saveCalls = calls.filter(e => e.target === 'save'); - const addressSave = saveCalls.find( - e => e.source === 'processUser' && e.targetFilePath.includes('Models'), - ); - expect(addressSave).toBeDefined(); - }); -}); - -// --------------------------------------------------------------------------- -// PHP -// --------------------------------------------------------------------------- - -describe('Field type resolution (PHP)', () => { - let result: PipelineResult; - - beforeAll(async () => { - result = await runPipelineFromRepo( - path.join(FIXTURES, 'php-field-types'), - () => {}, - ); - }, 60000); - - it('detects classes: Address, Service, User', () => { - expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']); - }); - - it('detects Property nodes for PHP properties', () => { - const properties = getNodesByLabel(result, 'Property'); - // PHP property_declaration was already captured pre-Phase 8 - expect(properties).toContain('address'); - expect(properties).toContain('name'); - expect(properties).toContain('city'); - }); - - it('emits HAS_PROPERTY edges linking properties to classes', () => { - const propEdges = getRelationships(result, 'HAS_PROPERTY'); - expect(propEdges.length).toBeGreaterThanOrEqual(3); - }); - - it('resolves $user->address->save() → Address#save via field type', () => { - const calls = getRelationships(result, 'CALLS'); - const saveCalls = calls.filter(e => e.target === 'save'); - const addressSave = saveCalls.find( - e => e.source === 'processUser' && e.targetFilePath.includes('Models'), - ); - expect(addressSave).toBeDefined(); - }); -}); diff --git a/gitnexus/test/integration/resolvers/go.test.ts b/gitnexus/test/integration/resolvers/go.test.ts index 737235666..1c01882f5 100644 --- a/gitnexus/test/integration/resolvers/go.test.ts +++ b/gitnexus/test/integration/resolvers/go.test.ts @@ -940,3 +940,88 @@ describe('Go for-loop call_expression iterable resolution (Phase 7.3)', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (Go)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'go-field-types'), + () => {}, + ); + }, 60000); + + it('detects structs: Address, User', () => { + expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'User']); + }); + + it('detects Property nodes for Go struct fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('Address'); + expect(properties).toContain('Name'); + expect(properties).toContain('City'); + }); + + it('emits HAS_PROPERTY edges linking struct fields to structs', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(2); + }); + + it('resolves user.Address.Save() → Address#Save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'Save'); + const addressSave = saveCalls.find( + e => e.source === 'processUser' && e.targetFilePath.includes('models'), + ); + expect(addressSave).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (Go)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'go-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects structs: Address, City, User', () => { + expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'City', 'User']); + }); + + it('detects Property nodes for Go struct fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('Address'); + expect(properties).toContain('City'); + expect(properties).toContain('ZipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + }); + + it('resolves 2-level chain: user.Address.Save() → Address#Save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'Save' && e.source === 'processUser'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('models')); + expect(addressSave).toBeDefined(); + }); + + it('resolves 3-level chain: user.Address.City.GetName() → City#GetName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'GetName' && e.source === 'processUser'); + const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('models')); + expect(cityGetName).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index a65102084..8583b5bbf 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -1068,3 +1068,93 @@ describe('Java foreach call_expression iterable resolution (Phase 7.3)', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (Java)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'java-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, App, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'App', 'User']); + }); + + it('detects Property nodes for Java fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); + + it('resolves user.address.save() → Address#save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save'); + const addressSave = saveCalls.find( + e => e.source === 'processUser' && e.targetFilePath.includes('Address'), + ); + expect(addressSave).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (Java)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'java-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, App, City, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'App', 'City', 'User']); + }); + + it('detects Property nodes for Java fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('city'); + expect(properties).toContain('zipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('Address → city'); + expect(edgeSet(propEdges)).toContain('City → zipCode'); + }); + + it('resolves 2-level chain: user.address.save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processUser'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('Address')); + expect(addressSave).toBeDefined(); + }); + + it('resolves 3-level chain: user.address.city.getName() → City#getName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'getName' && e.source === 'processUser'); + const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('City')); + expect(cityGetName).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/javascript.test.ts b/gitnexus/test/integration/resolvers/javascript.test.ts index 58f69e8bd..ee6bd8d8e 100644 --- a/gitnexus/test/integration/resolvers/javascript.test.ts +++ b/gitnexus/test/integration/resolvers/javascript.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'path'; import { - FIXTURES, getRelationships, getNodesByLabel, + FIXTURES, getRelationships, getNodesByLabel, edgeSet, runPipelineFromRepo, type PipelineResult, } from './helpers.js'; @@ -234,3 +234,37 @@ describe('JavaScript chained method call resolution', () => { expect(repoSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution — class field_definition capture +// --------------------------------------------------------------------------- + +describe('Field type resolution (JavaScript)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'js-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, Config, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Config', 'User']); + }); + + it('detects Property nodes for JS class fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking fields to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); +}); diff --git a/gitnexus/test/integration/resolvers/kotlin.test.ts b/gitnexus/test/integration/resolvers/kotlin.test.ts index 171872c95..743b22466 100644 --- a/gitnexus/test/integration/resolvers/kotlin.test.ts +++ b/gitnexus/test/integration/resolvers/kotlin.test.ts @@ -1218,3 +1218,93 @@ describe('Kotlin for-loop call_expression iterable resolution (Phase 7.3)', () = expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (Kotlin)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'kotlin-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']); + }); + + it('detects Property nodes for Kotlin properties', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); + + it('resolves user.address.save() → Address#save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save'); + const addressSave = saveCalls.find( + e => e.source === 'processUser' && e.targetFilePath.includes('Models'), + ); + expect(addressSave).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (Kotlin)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'kotlin-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, City, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'User']); + }); + + it('detects Property nodes for Kotlin properties', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('city'); + expect(properties).toContain('zipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('Address → city'); + expect(edgeSet(propEdges)).toContain('City → zipCode'); + }); + + it('resolves 2-level chain: user.address.save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processUser'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('Models')); + expect(addressSave).toBeDefined(); + }); + + it('resolves 3-level chain: user.address.city.getName() → City#getName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'getName' && e.source === 'processUser'); + const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('Models')); + expect(cityGetName).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/php.test.ts b/gitnexus/test/integration/resolvers/php.test.ts index a8cc100f7..3b8778504 100644 --- a/gitnexus/test/integration/resolvers/php.test.ts +++ b/gitnexus/test/integration/resolvers/php.test.ts @@ -1161,3 +1161,88 @@ describe('PHP foreach call_expression iterable resolution (Phase 7.3)', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (PHP)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'php-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, Service, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']); + }); + + it('detects Property nodes for PHP properties', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + }); + + it('resolves $user->address->save() → Address#save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save'); + const addressSave = saveCalls.find( + e => e.source === 'processUser' && e.targetFilePath.includes('Models'), + ); + expect(addressSave).toBeDefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (PHP)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'php-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, City, Service, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'Service', 'User']); + }); + + it('detects Property nodes for PHP properties', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('city'); + expect(properties).toContain('zipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + }); + + it('resolves 2-level chain: $user->address->save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processUser'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('Models')); + expect(addressSave).toBeDefined(); + }); + + it('resolves 3-level chain: $user->address->city->getName() → City#getName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'getName' && e.source === 'processUser'); + const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('Models')); + expect(cityGetName).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/python.test.ts b/gitnexus/test/integration/resolvers/python.test.ts index 572f86ea9..84285919e 100644 --- a/gitnexus/test/integration/resolvers/python.test.ts +++ b/gitnexus/test/integration/resolvers/python.test.ts @@ -1224,3 +1224,37 @@ describe('Python for-loop call_expression iterable resolution (Phase 7.3)', () = expect(saveCalls.length).toBe(1); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution — annotated attribute capture +// --------------------------------------------------------------------------- + +describe('Field type resolution (Python)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'python-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']); + }); + + it('detects Property nodes for Python annotated attributes', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking attributes to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); +}); diff --git a/gitnexus/test/integration/resolvers/ruby.test.ts b/gitnexus/test/integration/resolvers/ruby.test.ts index db06830a8..6fd0a7ee1 100644 --- a/gitnexus/test/integration/resolvers/ruby.test.ts +++ b/gitnexus/test/integration/resolvers/ruby.test.ts @@ -846,3 +846,46 @@ describe('Ruby for-in loop resolution', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution via YARD @return annotations +// --------------------------------------------------------------------------- + +describe('Field type resolution (Ruby)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'ruby-field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']); + }); + + it('detects Property nodes for attr_accessor fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); + + it('resolves user.address.save → Address#save via YARD @return [Address]', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save'); + const addressSave = saveCalls.find( + e => e.source === 'process_user' && e.targetFilePath.includes('models'), + ); + expect(addressSave).toBeDefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index f44a076c3..aa6f6c2d1 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -832,10 +832,25 @@ describe('Rust nullable receiver resolution (Option)', () => { expect(saveFns.length).toBe(2); }); - // Known limitation: user.unwrap().save() chains two method calls. unwrap() - // returns User but TypeEnv doesn't track intermediate return values in chains. - // Disambiguating through .unwrap() requires chained return type inference (Phase 5). - it.todo('resolves user.unwrap().save() to User.save (requires chained call inference)'); + it('resolves user.unwrap().save() to User#save via Option unwrapping', () => { + const calls = getRelationships(result, 'CALLS'); + const userSave = calls.find(c => + c.target === 'save' && + c.source === 'process_entities' && + c.targetFilePath?.includes('user'), + ); + expect(userSave).toBeDefined(); + }); + + it('resolves repo.unwrap().save() to Repo#save via Option unwrapping', () => { + const calls = getRelationships(result, 'CALLS'); + const repoSave = calls.find(c => + c.target === 'save' && + c.source === 'process_entities' && + c.targetFilePath?.includes('repo'), + ); + expect(repoSave).toBeDefined(); + }); }); // --------------------------------------------------------------------------- @@ -1279,3 +1294,34 @@ describe('Rust for-loop direct call_expression iterable resolution (Phase 7.3)', expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution — struct field capture +// --------------------------------------------------------------------------- + +describe('Field type resolution (Rust)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'rust-field-types'), + () => {}, + ); + }, 60000); + + it('detects structs: Address, User', () => { + expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'User']); + }); + + it('detects Property nodes for Rust struct fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking fields to structs', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(2); + }); +}); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 45f1c8cd3..04fc472bc 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -1698,3 +1698,205 @@ describe('TypeScript for-of call_expression iterable resolution (Phase 7.3)', () expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Phase 8: Field/property type resolution (1-level) +// --------------------------------------------------------------------------- + +describe('Field type resolution (TypeScript)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'field-types'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, Config, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Config', 'User']); + }); + + it('detects Property nodes for typed fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('name'); + expect(properties).toContain('city'); + }); + + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(propEdges.length).toBeGreaterThanOrEqual(3); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('Address → city'); + }); + + it('resolves user.address.save() → Address#save via field type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save'); + const addressSave = saveCalls.find(e => e.targetFilePath.includes('models')); + expect(addressSave).toBeDefined(); + expect(addressSave!.source).toBe('processUser'); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8: Field type disambiguation — both User and Address have save() +// --------------------------------------------------------------------------- + +describe('Field type disambiguation (TypeScript)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'ts-field-type-disambig'), + () => {}, + ); + }, 60000); + + it('detects both User#save and Address#save', () => { + const methods = getNodesByLabel(result, 'Method'); + const saveMethods = methods.filter(m => m === 'save'); + expect(saveMethods.length).toBe(2); + }); + + it('resolves user.address.save() → Address#save (not User#save)', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter( + e => e.target === 'save' && e.source === 'processUser', + ); + expect(saveCalls.length).toBe(1); + expect(saveCalls[0].targetFilePath).toContain('address'); + expect(saveCalls[0].targetFilePath).not.toContain('user'); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8: Parameter properties and #private fields +// --------------------------------------------------------------------------- + +describe('Field type resolution (TS parameter properties)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'ts-param-property-fields'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']); + }); + + it('captures constructor parameter properties as Property nodes', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('name'); + expect(properties).toContain('address'); + }); + + it('captures #private fields as Property nodes', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('#secret'); + }); + + it('emits HAS_PROPERTY edges for parameter properties', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toContain('User → name'); + expect(edgeSet(propEdges)).toContain('User → address'); + }); + + it('resolves user.address.save() via parameter property type', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processUser'); + expect(saveCalls.length).toBe(1); + expect(saveCalls[0].targetFilePath).toContain('models'); + }); +}); + +// --------------------------------------------------------------------------- +// Phase 8A: Deep field chain resolution (3-level: user.address.city.getName()) +// --------------------------------------------------------------------------- + +describe('Deep field chain resolution (TypeScript)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'ts-deep-field-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, City, User', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'User']); + }); + + it('detects Property nodes for all typed fields', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('address'); + expect(properties).toContain('city'); + expect(properties).toContain('zipCode'); + }); + + it('emits HAS_PROPERTY edges for nested type chain', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); + expect(edgeSet(propEdges)).toContain('User → address'); + expect(edgeSet(propEdges)).toContain('Address → city'); + expect(edgeSet(propEdges)).toContain('City → zipCode'); + }); + + it('resolves 2-level chain: user.address.save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processUser'); + expect(saveCalls.length).toBe(1); + expect(saveCalls[0].targetFilePath).toContain('models'); + }); + + it('resolves 3-level chain: user.address.city.getName() → City#getName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'getName' && e.source === 'processUser'); + expect(getNameCalls.length).toBe(1); + expect(getNameCalls[0].targetFilePath).toContain('models'); + }); +}); + +// --------------------------------------------------------------------------- +// Mixed chain resolution (field ↔ call interleaved) +// --------------------------------------------------------------------------- + +describe('Mixed field+call chain resolution (TypeScript)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'ts-mixed-chain'), + () => {}, + ); + }, 60000); + + it('detects classes: Address, City, User, UserService', () => { + expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'User', 'UserService']); + }); + + it('detects Property node for Address.city field', () => { + const properties = getNodesByLabel(result, 'Property'); + expect(properties).toContain('city'); + expect(properties).toContain('address'); + }); + + it('resolves call→field chain: svc.getUser().address.save() → Address#save', () => { + const calls = getRelationships(result, 'CALLS'); + const saveCalls = calls.filter(e => e.target === 'save' && e.source === 'processWithService'); + expect(saveCalls.length).toBe(1); + expect(saveCalls[0].targetFilePath).toContain('models'); + }); + + it('resolves field→call chain: user.getAddress().city.getName() → City#getName', () => { + const calls = getRelationships(result, 'CALLS'); + const getNameCalls = calls.filter(e => e.target === 'getName' && e.source === 'processWithUser'); + expect(getNameCalls.length).toBe(1); + expect(getNameCalls[0].targetFilePath).toContain('models'); + }); +}); diff --git a/type-resolution-roadmap.md b/type-resolution-roadmap.md index 2da9077b0..50485c1ab 100644 --- a/type-resolution-roadmap.md +++ b/type-resolution-roadmap.md @@ -107,32 +107,76 @@ The interface change touched all extractors but remained additive — no existin --- -## Phase 8: Field and Property Type Resolution +## Phase 8: Field and Property Type Resolution *(delivered)* ### Goal Model class / struct fields so chained member access can be resolved more accurately. +### Status + +**Delivered.** One-level, deep, and mixed field+method chain resolution is implemented across 10 languages. Pattern destructuring (8C) remains open. + +#### What shipped + +- **SymbolTable `fieldByOwner` index** — O(1) lookup via `ownerNodeId\0fieldName` key. Properties excluded from `globalIndex` to prevent namespace pollution. *(Q1 resolved)* +- **`HAS_PROPERTY` edge type** — split from `HAS_METHOD` to distinguish property linkage +- **`declaredType` field** on Property symbols — semantic split from `returnType` (methods) +- **`resolveFieldAccessType`** in call-processor — resolves field access chains at call sites +- **`extractPropertyDeclaredType`** in shared utils — 5-strategy cross-language type extraction +- **Per-language `@definition.property` captures** — see coverage table below +- **`extractMixedChain`** in utils — unified recursive AST walker that handles both `call_expression` and `field_expression` nodes interchangeably, building `MixedChainStep[]` capped at `MAX_CHAIN_DEPTH` (3). Replaces the earlier separate `extractFieldChain` / `extractCallChain` functions. +- **`receiverMixedChain`** on `ExtractedCall` — unified chain representation replacing the old `receiverCallChain` + `receiverFieldAccess` split +- **Unified chain resolution** in call-processor — a single loop in both `processCalls` (sequential) and `processCallsFromExtracted` (worker) walks `MixedChainStep[]`, dispatching `kind: 'field'` to `resolveFieldAccessType` and `kind: 'call'` to `resolveCallTarget` + return type extraction +- **Type-preserving stdlib passthrough** — `unwrap()`, `expect()`, `clone()`, `as_ref()`, and similar stdlib methods that don't change the receiver type are recognized as identity operations in the chain loop, allowing chains like `user.unwrap().save()` to resolve correctly when TypeEnv has already stripped the nullable wrapper +- **C++ `field_declaration`** property capture via `field_identifier` declarator +- **C++ `field_expression` support** — tree-sitter-cpp uses `argument` (not `object`) for the receiver of `field_expression`; `extractMixedChain` handles this +- **C++ inline method double-indexing guard** — prevents `@definition.function` from creating duplicate symbol entries for methods already captured by `@definition.method` inside class/struct bodies (applied in both `parsing-processor.ts` and `parse-worker.ts`) +- **Rust unit struct instantiation** — `let svc = UserService;` (bare identifier assignment) now recognized by type-env when the RHS matches a known class/struct name +- **Ruby YARD `@return [Type]`** extraction for `attr_accessor` properties, enabling field-type resolution in dynamically typed Ruby + +#### Language coverage + +| Language | Property capture | `declaredType` extraction | Deep chain | Notes | +|----------|-----------------|--------------------------|:----------:|-------| +| TypeScript | ✅ `public_field_definition`, `private_property_identifier`, `required_parameter` | ✅ Strategy 2 (type_annotation) | ✅ | Parameter properties added | +| JavaScript | ✅ `field_definition` | ⚠️ No type annotations in JS | — | Capture added; declaredType requires JSDoc | +| Java | ✅ `field_declaration` | ✅ Strategy 3 (parent type) | ✅ | | +| C# | ✅ `property_declaration` | ✅ Strategy 1 (type field) | ✅ | | +| Go | ✅ `field_declaration` | ✅ Strategy 1 (type field) | ✅ | | +| Kotlin | ✅ `property_declaration` | ✅ Strategy 4 (variable_declaration) | ✅ | New strategy added | +| PHP | ✅ `property_declaration` | ✅ Strategy 1 + PHPDoc @var fallback | ✅ | Strategy 5 for pre-7.4 | +| Rust | ✅ `field_declaration` | ✅ Strategy 1 (type field) | — | Capture only (no field-access call resolution) | +| Python | ✅ `assignment` with `type` | ⚠️ Class-level only | — | `self.x` pattern needs work | +| Ruby | ✅ `attr_*` via call routing | ✅ YARD `@return [Type]` | — | YARD fallback for dynamically typed properties | +| C++ | ✅ `field_declaration` via `field_identifier` | ✅ Strategy 1 (type field) | ✅ | | +| Swift | ✅ `property_declaration` | ⚠️ Untested | — | | + +#### What remains open + +- **8C. Pattern destructuring** dependent on field knowledge +- Python `self.x` instance attribute pattern + ### Problems this phase addresses -#### 8A. Deep property chains +#### 8A. Deep property chains *(delivered)* ```typescript -user.address.city +user.address.city.getName() ``` -Today the system may resolve `user -> User`, but it cannot generally resolve: +✅ `extractFieldChain` recursively walks nested member_expression nodes at parse time, building a `fieldChain: string[]`. At resolution time, the chain is walked step-by-step: `user → User`, `address → Address`, `city → City`, `getName() → City#getName`. Supported across TS, Java, C#, Go, Kotlin, PHP, C++. -- `address -> Address` -- `city -> City` or scalar type - -#### 8B. Chained method targets through field access +#### 8B. Mixed field+method chain resolution *(delivered)* ```typescript -user.address.save() +svc.getUser().address.save() // call → field → call +user.getAddress().city.getName() // call → field → call +user.address.getCity().save() // field → call → call +user.unwrap().save() // stdlib passthrough → call ``` -Without field typing, the resolver cannot reliably identify the receiver type of `address`. +✅ `extractMixedChain` walks both call-expression and field-expression nodes in a single unified pass, producing `MixedChainStep[]`. The resolver walks steps left-to-right: `kind: 'field'` resolves via `resolveFieldAccessType`, `kind: 'call'` resolves via `resolveCallTarget` + return type extraction. Stdlib passthroughs (`unwrap`, `clone`, `expect`, etc.) are recognized as type-preserving identity operations. #### 8C. Pattern destructuring that depends on field knowledge @@ -142,32 +186,42 @@ This is especially relevant for: - PHP chained property access - richer TypeScript or Python object-based destructuring in future work -### Engineering direction +### Engineering direction (as implemented) -- parse field / property declarations per class or struct -- build a field-type map keyed by owning type -- teach lookup and chain-resolution logic to walk member segments +- ~~parse field / property declarations per class or struct~~ ✅ +- ~~build a field-type map keyed by owning type~~ ✅ (`fieldByOwner` index) +- ~~teach lookup and chain-resolution logic to walk member segments (deep chains)~~ ✅ (`extractMixedChain` + unified chain-walking loop) +- ~~unify field chains and call chains into a single representation~~ ✅ (`MixedChainStep[]` replaces separate `receiverCallChain` / `receiverFieldAccess`) +- ~~C++ struct member field capture~~ ✅ (`field_declaration` via `field_identifier`) +- ~~C++ `field_expression` receiver extraction~~ ✅ (`argument` field support in `extractMixedChain`) +- ~~Rust unit struct instantiation~~ ✅ (`let svc = TypeName;` recognized by type-env) +- ~~Ruby YARD `@return` for `attr_accessor`~~ ✅ (comment-walking in `call-routing.ts`) +- ~~stdlib passthrough methods~~ ✅ (`TYPE_PRESERVING_METHODS` set in call-processor) - keep this separate from the base variable-binding layer where possible -### Expected impact +### Delivered impact This is the biggest unlock for richer static analysis because it allows the graph to model more than just top-level receivers. -It would materially improve: +It materially improved: -- chained property resolution -- member-based call disambiguation +- chained property resolution (up to 3 levels deep) +- mixed field+method chain resolution (e.g. `svc.getUser().address.save()`) +- member-based call disambiguation across 10 languages - deeper context extraction for downstream tooling +- C++ struct/class field visibility in the knowledge graph +- C++ chained method call resolution (previously blocked by missing `argument` field support) +- Rust nullable receiver chains (`user.unwrap().save()`) +- Ruby field-type resolution via YARD documentation ### Risk level -**High** +**High** (delivered — risk was managed through incremental delivery across 8, 8A, 8B) -This is the first phase that pushes the system from variable typing into structural object modelling. It will likely require: +This phase pushed the system from variable typing into structural object modelling. Remaining work: -- schema expansion or new internal maps - careful handling of inheritance / embedding / language-specific member semantics -- broader test coverage than earlier phases +- pattern destructuring dependent on field knowledge (8C) --- @@ -282,44 +336,33 @@ Key remaining gap: Shared missing capabilities: -- field / property type resolution -- generalised return-type-aware binding in `TypeEnv` +- ~~field / property type resolution~~ ✓ shipped in Phase 8 + 8A (10 languages) +- ~~mixed field+method chain resolution~~ ✓ shipped in Phase 8B (unified `MixedChainStep[]`) +- generalised return-type-aware binding in `TypeEnv` (Phase 9) -**Priority:** Very High -**Reason:** These are the biggest remaining blockers to deeper static analysis. +**Priority:** High +**Reason:** Return-type propagation is the biggest remaining blocker to deeper static analysis. --- ## Recommended Delivery Order -### 1. Generalise existing return and loop inference +### ~~1. Generalise existing return and loop inference~~ ✅ Phase 7 -This is the best cost-to-value step. +Delivered. Iterable call-expression support, `ReturnTypeLookup`, file-scope binding, PHP Strategy C. -Deliverables: +### ~~2. Add field / property type maps~~ ✅ Phase 8 + 8A + 8B -- iterable call-expression support -- wider access to return-type maps -- file-scope binding visibility where needed +Delivered. Per-type field metadata, deep chain resolution (up to 3 levels), mixed field+method chains, type-preserving stdlib passthrough, C++ and Rust fixes. -### 2. Add field / property type maps - -This unlocks the next class of analysis depth. - -Deliverables: - -- per-type field metadata -- chained property resolution -- better destructuring support - -### 3. Promote return types into first-class `TypeEnv` inputs +### 3. Promote return types into first-class `TypeEnv` inputs ← **next** This converts existing downstream validation into a broader inference capability. Deliverables: -- call-result variable binding -- loop inference from call results +- call-result variable binding (`var x = f()` propagation) +- loop inference from call results (already done for direct iterables, pending for assigned results) - broader chain propagation ### 4. Broaden branch-sensitive narrowing where low-risk @@ -352,23 +395,25 @@ That would be sufficient for: ## Suggested Milestone Definitions -### Milestone A — Inference Expansion +### Milestone A — Inference Expansion ✅ -Success looks like: +Delivered in Phase 7. -- loop inference works for identifier iterables and common call-expression iterables -- simple call-result assignments benefit from return types more broadly -- no major regression in ambiguity handling +- loop inference works for identifier iterables and common call-expression iterables across 7 languages +- `ReturnTypeLookup` threads return-type knowledge into TypeEnv +- PHP class-level `@var` property typing for `$this->property` foreach -### Milestone B — Structural Member Typing +### Milestone B — Structural Member Typing ✅ -Success looks like: +Delivered in Phase 8 + 8A + 8B. -- field/property maps exist for class-like types -- chained access can resolve at least one segment beyond the base receiver -- field-aware member-call resolution works in the most important languages +- field/property maps exist for class-like types across 10 languages +- deep chains resolve up to 3 levels (`user.address.city.getName()`) +- mixed field+method chains resolve interleaved patterns (`svc.getUser().address.save()`) +- stdlib passthroughs (`unwrap`, `clone`, etc.) are type-preserving in chains +- C++ and Rust chain call resolution fixed (field_expression argument, unit struct) -### Milestone C — Static-Analysis Foundation +### Milestone C — Static-Analysis Foundation ← **next** Success looks like: @@ -382,8 +427,8 @@ Success looks like: These should be resolved before or during implementation of the later phases. -1. **Where should field-type metadata live?** - In `TypeEnv`, in `SymbolTable`, or in a dedicated side structure? +1. **Where should field-type metadata live?** + ✅ Resolved: in `SymbolTable` via the `fieldByOwner` index, keyed by `ownerNodeId\0fieldName`. Properties live alongside other symbols but are excluded from `globalIndex` to prevent namespace pollution. 2. **How should ambiguity be represented?** Is `undefined` sufficient, or do later phases need a richer "known ambiguous" state? @@ -394,19 +439,23 @@ These should be resolved before or during implementation of the later phases. 4. **How much branch sensitivity is worth the complexity?** Some narrowing gives clear value; full control-flow typing likely does not. -5. **Should field typing and chain typing be one phase or two?** - Keeping them separate may reduce risk and make regressions easier to isolate. +5. **Should field typing and chain typing be one phase or two?** + ✅ Resolved: delivered as Phase 8 (single-level) + Phase 8A (deep chains) in the same branch, with separate test suites per language. Incremental delivery within one phase worked well. --- ## Summary -The next stage of the type system should focus on **generalising what already works** before attempting compiler-like sophistication. +Phases 7 and 8 (including 8A and 8B) are **complete**. The type system now handles: -The most important path is: +- ✅ explicit type annotations and parameters across 13 languages +- ✅ initializer/constructor inference with SymbolTable validation +- ✅ loop element inference including call-expression iterables (7 languages) +- ✅ field/property type resolution with deep chains (up to 3 levels, 10 languages) +- ✅ mixed field+method chains (`svc.getUser().address.save()`) +- ✅ type-preserving stdlib passthroughs (`unwrap`, `clone`, `expect`, etc.) +- ✅ comment-based types (JSDoc, PHPDoc, YARD) -1. extend return-type and iterable inference -2. add field/property type knowledge -3. promote return-type-aware inference into `TypeEnv` +**The next step is Phase 9**: promote return-type-aware inference into `TypeEnv` as a first-class input, enabling `var x = f()` variable binding and broader chain propagation. The `pendingCallResults` infrastructure is already in place (Tier 2b loop + `PendingAssignment` union) — it just needs extractors to emit `{ kind: 'callResult' }` entries. -That path preserves the current strengths of the system while moving GitNexus materially closer to a robust, production-grade static-analysis foundation. +That path preserves the current strengths of the system while moving GitNexus the final step toward a robust, production-grade static-analysis foundation. diff --git a/type-resolution-system.md b/type-resolution-system.md index ab221155c..bd20bd38e 100644 --- a/type-resolution-system.md +++ b/type-resolution-system.md @@ -122,7 +122,6 @@ It does not: - perform full semantic type checking - run fixpoint inference - propagate inferred bindings across files as ordinary environment entries -- model deep field/property chains such as `user.address.city` - guarantee resolution for every ambiguous construct --- @@ -375,22 +374,28 @@ So return-type-aware receiver inference already exists in a constrained downstre | For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | | Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | No | | Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | +| Field/property type resolution | Yes | Yes | Yes | Yes | Yes | Yes | No* | Yes | YARD | No | Yes | | Comment-based types | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | | Return type extraction | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | +\* Python has a type annotation query for fields but no `declaredType` extraction for the `self.x` pattern yet. + --- ## Current Strengths -The current system already provides strong value for call resolution because it combines: +The current system provides strong value for call resolution because it combines: -- explicit annotation extraction -- generic-aware loop element typing -- initializer-based inference +- explicit annotation extraction across 13 languages +- generic-aware loop element typing (including call-expression iterables) +- initializer-based inference with SymbolTable validation - selected pattern-based narrowing - scope-aware lookups -- comment-based fallbacks for dynamic ecosystems +- comment-based fallbacks for dynamic ecosystems (JSDoc, PHPDoc, YARD) - constrained return-type-aware receiver inference in call processing +- deep field/property chains up to 3 levels across 10 languages +- mixed field+method chain resolution (e.g. `svc.getUser().address.save()`) +- type-preserving stdlib passthrough for `unwrap()`, `clone()`, `expect()`, etc. This is enough to materially improve call-edge precision even without implementing a full static type system. @@ -400,7 +405,6 @@ This is enough to materially improve call-edge precision even without implementi Important gaps still remain: -- no field / property type map for deep chains such as `user.address.city` - no general cross-file propagation of inferred bindings - no fixpoint inference - limited branch-sensitive narrowing outside selected pattern constructs