mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat: Phase 8B mixed field+method chain resolution, C++/Rust chain fixes
Unify field and method chain resolution into a single `extractMixedChain` walker that handles interleaved patterns like `svc.getUser().address.save()`. Fix C++ chain calls (tree-sitter-cpp `field_expression` uses `argument` not `object`), Rust unit struct instantiation (`let svc = TypeName;`), and add stdlib passthrough for `unwrap()`/`clone()`/`expect()` in chain loops. Key changes: - Replace `receiverCallChain` + `receiverFieldAccess` with unified `receiverMixedChain: MixedChainStep[]` on ExtractedCall - Add `extractMixedChain` in utils.ts (handles both call_expression and field_expression nodes, including C++ `argument` field) - Add `TYPE_PRESERVING_METHODS` set for stdlib identity operations - Add C++ inline method double-indexing guard in parsing-processor.ts and parse-worker.ts - Add Rust unit struct recognition in type-extractors/rust.ts - Split field-types.test.ts into per-language test files - Add ts-mixed-chain fixture and integration tests - Resolve rust.test.ts todo: Option<T>.unwrap().save() now works - Update roadmap: Phases 7+8 complete, Phase 9 is next
This commit is contained in:
parent
c6ca847641
commit
8ef6911a8f
57 changed files with 1955 additions and 642 deletions
|
|
@ -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> → 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<User>
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>" → "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 } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
|
|||
};
|
||||
|
||||
/** Rust: let x = User::new(), let x = User::default(), or let x = User { ... } */
|
||||
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, _classNames: ClassNameLookup): void => {
|
||||
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, 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<str
|
|||
return;
|
||||
}
|
||||
|
||||
// Unit struct instantiation: let svc = UserService; (bare identifier, no braces or call)
|
||||
if (value.type === 'identifier' && classNames.has(value.text)) {
|
||||
const varName = extractVarName(pattern);
|
||||
if (varName) env.set(varName, value.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.type !== 'call_expression') return;
|
||||
const func = value.childForFieldName('function');
|
||||
if (!func || func.type !== 'scoped_identifier') return;
|
||||
|
|
|
|||
|
|
@ -745,3 +745,91 @@ export const extractReturnTypeName = (raw: string, depth = 0): string | undefine
|
|||
|
||||
return text;
|
||||
};
|
||||
|
||||
// ── Property declared-type extraction ────────────────────────────────────
|
||||
// Shared between parse-worker (worker path) and parsing-processor (sequential path).
|
||||
|
||||
/**
|
||||
* Extract the declared type of a property/field from its AST definition node.
|
||||
* Handles cross-language patterns:
|
||||
* - 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
|
||||
* - Kotlin: `var name: Type` → variable_declaration child with type field
|
||||
*
|
||||
* Returns the normalized type name, or undefined if no type can be extracted.
|
||||
*/
|
||||
export 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<User>)
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<User>)
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "service.h"
|
||||
#include "user.h"
|
||||
#include "repo.h"
|
||||
|
||||
void processUser() {
|
||||
|
|
|
|||
30
gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/models.h
vendored
Normal file
30
gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/models.h
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/service.cpp
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-deep-field-chain/service.cpp
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
20
gitnexus/test/fixtures/lang-resolution/cpp-field-types/models.h
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/cpp-field-types/models.h
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-field-types/service.cpp
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-field-types/service.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "models.h"
|
||||
|
||||
void processUser(User user) {
|
||||
// Field-access chain: user.address → Address, then .save() → Address#save
|
||||
user.address.save();
|
||||
}
|
||||
33
gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Models.cs
vendored
Normal file
33
gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Models.cs
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Service.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-deep-field-chain/Service.cs
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/cmd/main.go
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/cmd/main.go
vendored
Normal file
|
|
@ -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()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/go-deep-field-chain
|
||||
|
||||
go 1.21
|
||||
27
gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/models/models.go
vendored
Normal file
27
gitnexus/test/fixtures/lang-resolution/go-deep-field-chain/models/models.go
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/App.java
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/App.java
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/Address.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/Address.java
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package models;
|
||||
|
||||
public class Address {
|
||||
public City city;
|
||||
public String street;
|
||||
|
||||
public void save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/City.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/City.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models;
|
||||
|
||||
public class City {
|
||||
public String zipCode;
|
||||
|
||||
public String getName() {
|
||||
return "city";
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/User.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-deep-field-chain/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public String name;
|
||||
public Address address;
|
||||
|
||||
public String greet() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
26
gitnexus/test/fixtures/lang-resolution/js-field-types/models.js
vendored
Normal file
26
gitnexus/test/fixtures/lang-resolution/js-field-types/models.js
vendored
Normal file
|
|
@ -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 };
|
||||
9
gitnexus/test/fixtures/lang-resolution/js-field-types/service.js
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/js-field-types/service.js
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { User, Config } = require('./models');
|
||||
|
||||
function processUser(user) {
|
||||
user.address.save();
|
||||
}
|
||||
|
||||
function validateConfig() {
|
||||
Config.DEFAULT.validate();
|
||||
}
|
||||
25
gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Models.kt
vendored
Normal file
25
gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Models.kt
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Service.kt
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/kotlin-deep-field-chain/Service.kt
vendored
Normal file
|
|
@ -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()
|
||||
}
|
||||
34
gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Models.php
vendored
Normal file
34
gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Models.php
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
class City {
|
||||
/** @var string */
|
||||
public string $zipCode;
|
||||
|
||||
public function getName(): string {
|
||||
return "city";
|
||||
}
|
||||
}
|
||||
|
||||
class Address {
|
||||
/** @var City */
|
||||
public City $city;
|
||||
|
||||
/** @var string */
|
||||
public string $street;
|
||||
|
||||
public function save(): void {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
/** @var string */
|
||||
public string $name;
|
||||
|
||||
/** @var Address */
|
||||
public Address $address;
|
||||
|
||||
public function greet(): string {
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Service.php
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/php-deep-field-chain/Service.php
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
class Service {
|
||||
public function processUser(User $user): void {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/python-field-types/models.py
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/python-field-types/models.py
vendored
Normal file
|
|
@ -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
|
||||
4
gitnexus/test/fixtures/lang-resolution/python-field-types/service.py
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/python-field-types/service.py
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from models import User
|
||||
|
||||
def process_user(user: User):
|
||||
user.address.save()
|
||||
20
gitnexus/test/fixtures/lang-resolution/ruby-field-types/models.rb
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/ruby-field-types/models.rb
vendored
Normal file
|
|
@ -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
|
||||
7
gitnexus/test/fixtures/lang-resolution/ruby-field-types/service.rb
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ruby-field-types/service.rb
vendored
Normal file
|
|
@ -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
|
||||
20
gitnexus/test/fixtures/lang-resolution/rust-field-types/models.rs
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/rust-field-types/models.rs
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/rust-field-types/service.rs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/rust-field-types/service.rs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
use crate::models::{User, Address};
|
||||
|
||||
fn process_user(user: &User) {
|
||||
user.address.save();
|
||||
}
|
||||
25
gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/models.ts
vendored
Normal file
25
gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/models.ts
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/service.ts
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/ts-deep-field-chain/service.ts
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/address.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/address.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export class Address {
|
||||
city: string;
|
||||
|
||||
save(): void {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/service.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/service.ts
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/user.ts
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/ts-field-type-disambig/user.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Address } from './address';
|
||||
|
||||
export class User {
|
||||
name: string;
|
||||
address: Address;
|
||||
|
||||
save(): void {
|
||||
// persist user
|
||||
}
|
||||
}
|
||||
27
gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/models.ts
vendored
Normal file
27
gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/models.ts
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/service.ts
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/ts-mixed-chain/service.ts
vendored
Normal file
|
|
@ -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();
|
||||
}
|
||||
22
gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/models.ts
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/models.ts
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/service.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/ts-param-property-fields/service.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { User } from './models';
|
||||
|
||||
function processUser(user: User) {
|
||||
user.address.save();
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -832,10 +832,25 @@ describe('Rust nullable receiver resolution (Option<T>)', () => {
|
|||
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<User> 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<Repo> 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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue