mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat: Phase 8 field/property type resolution (#354)
* feat: Phase 8 field/property type resolution — resolve chained member access
Add field/property type extraction to the type resolution system so that
chained member access like `user.address.save()` resolves the intermediate
receiver type (`address → Address`) through Property symbols in SymbolTable.
Key changes:
- SymbolTable: add `declaredType` field, `fieldByOwner` O(1) index,
`lookupFieldByOwner()` method, P0 conditional callableIndex invalidation,
P2 exclude Properties from globalIndex to prevent namespace pollution
- tree-sitter queries: add `definition.property` for TypeScript, Java, Go
- parse-worker: extract declared types for Property nodes via
`extractPropertyDeclaredType()`, capture field-access receiver info
- call-processor: add `resolveFieldAccessType()` helper and field-access
branch in both sequential and worker receiver resolution paths
- Integration tests: new field-types test suite verifying end-to-end
`user.address.save() → Address#save` resolution
* fix: Go tree-sitter query captures field_declaration not field_declaration_list
Post-review fix: the Go struct field query incorrectly put @definition.property
on field_declaration_list (the list container) instead of field_declaration
(the individual field). Also removed unused `language` parameter from
extractPropertyDeclaredType.
* feat: expand field-type tests to 6 languages, fix Go ownerId and Kotlin navigation_expression
- Add integration test fixtures for Java, C#, Go, Kotlin, PHP (alongside existing TS)
- Fix Go: add type_declaration handling in findEnclosingClassId for struct fields
(field_declaration → field_declaration_list → struct_type → type_spec → type_declaration)
- Fix Kotlin: add navigation_expression handling in field-access resolution
(Kotlin uses navigation_expression + navigation_suffix, not member_expression)
- Add extractMemberAccessParts helper in call-processor for cross-language member access
- All 24 field-type tests pass across 6 languages, 181 Go+Kotlin tests pass with no regressions
* refactor: split HAS_METHOD into HAS_METHOD + HAS_PROPERTY edge types
Property nodes now use HAS_PROPERTY edges instead of HAS_METHOD, giving
the graph schema proper semantic separation between methods and fields.
- HAS_METHOD: Method, Constructor, Function (when inside a class)
- HAS_PROPERTY: Property nodes (class fields, struct fields, attributes)
MRO processor only reads HAS_METHOD — properties correctly excluded from
method resolution order. Impact analysis accepts both edge types.
Updated 12 files: graph types, schema, tools docs, parse-worker,
parsing-processor, call-processor, and 6 test files.
* fix(test): update security test to expect 7 VALID_RELATION_TYPES (added HAS_PROPERTY)
* test: add unit tests for Phase 8 SymbolTable features (39 tests, up from 19)
Cover all new branches: declaredType metadata, Property exclusion from
globalIndex, conditional callableIndex invalidation, lookupFieldByOwner
(happy path + edge cases), lookupFuzzyCallable filtering, and clear()
with fieldByOwner. Fixes branch coverage threshold (21.8% → 23%+).
* 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
* fix: Python declaredType extraction and sequential-path property registration
- Move @definition.property capture from expression_statement to assignment
node in Python queries so Strategy 1 childForFieldName('type') succeeds
- Pass item.declaredType through ctx.symbols.add in sequential call-processor
path, matching worker path behavior (fixes Ruby YARD declaredType drop)
- Add Python chain resolution integration test (user.address.save → Address#save)
- Update Rust/Python status in roadmap and system docs to reflect actual coverage
* fix: Python/Ruby field type disambiguation and Rust chain test
Three fixes from PR #354 third review:
1. Python typed_parameter name extraction: tree-sitter-python's
typed_parameter uses positional children for the name, not a named
field. TypeEnv and extractParameter now fall back to firstNamedChild.
2. Ruby/Python call-step field resolution: Ruby's AST uses `call` nodes
for both property access and method calls. The chain walker now tries
resolveFieldAccessType before resolveCallTarget for call steps, so
attr_accessor properties resolve via declaredType.
3. Rust chain resolution test: added missing integration test asserting
user.address.save() resolves to Address#save.
Also splits C/C++ and TS/JS columns in type-resolution-system.md
language matrix with footnotes for accuracy.
1062 resolver integration tests passing, 0 failures.
* refactor: Phase 8 code review cleanup — extract walkMixedChain, fix MCP agent gaps
- Extract duplicated chain resolution loop into shared walkMixedChain() helper,
eliminating ~60 lines of copy-pasted code between sequential and worker paths
- Add returnType to ResolveResult, removing redundant lookupFuzzy+find per chain step
- Fix context() tool to include HAS_METHOD, HAS_PROPERTY, OVERRIDES in queries
so agents can discover class members
- Fix p.declaredType Cypher example (column doesn't exist) → p.description
- Add HAS_METHOD, HAS_PROPERTY, OVERRIDES to schema resource
- Document HAS_METHOD/HAS_PROPERTY in impact tool description
- Delete dead code extractMemberAccessParts (superseded by extractMixedChain)
- Replace any with SyntaxNode on extractPropertyDeclaredType
- Add Rust deep-field-chain test (5 tests), Java mixed-chain (4), Go mixed-chain (4)
- All 1075 tests pass (13 new, 0 regressions)
* refactor: type SymbolDefinition.type as NodeLabel, add O(1) receiver index
- Change SymbolDefinition.type from string to NodeLabel union (35 members)
across symbol-table.ts, parse-worker.ts, parsing-processor.ts — compiler
now enforces correctness at all comparison/assignment sites
- Replace O(N*M) linear scan in lookupReceiverType with pre-built
ReceiverTypeIndex (Map<funcName, Map<varName, Entry>>) for O(1) lookups
with proper ambiguity handling and file-level fallback
- All 1075 tests pass, 0 regressions
* fix: capture C++ pointer/ref fields, Kotlin data class props, PHP constructor promotion
Add tree-sitter query patterns for three previously missed property declaration
forms: C++ pointer/reference member fields (Address* addr; Address& ref;),
Kotlin primary constructor val/var parameters (data class User(val name: String)),
and PHP 8.0+ constructor property promotion (public Address $address).
Fix "10 languages" off-by-one in docs (Ruby is single-level only, not deep chain).
Update Python feature matrix cell from No* to Yes* after 31b95f0 fix.
11 new integration tests with per-language fixtures verify property capture,
HAS_PROPERTY edge emission, and field-access chain resolution.
This commit is contained in:
parent
e0a6c40b45
commit
11a3d0515c
104 changed files with 3147 additions and 290 deletions
|
|
@ -80,6 +80,7 @@ export type RelationshipType =
|
|||
| 'IMPLEMENTS'
|
||||
| 'EXTENDS'
|
||||
| 'HAS_METHOD'
|
||||
| 'HAS_PROPERTY'
|
||||
| 'MEMBER_OF'
|
||||
| 'STEP_IN_PROCESS'
|
||||
|
||||
|
|
|
|||
|
|
@ -20,15 +20,25 @@ import {
|
|||
extractReceiverNode,
|
||||
findEnclosingClassId,
|
||||
CALL_EXPRESSION_TYPES,
|
||||
MAX_CHAIN_DEPTH,
|
||||
extractCallChain,
|
||||
extractMixedChain,
|
||||
type MixedChainStep,
|
||||
} 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.
|
||||
|
|
@ -178,6 +188,7 @@ export const processCalls = async (
|
|||
const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
|
||||
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
||||
: new Map<string, string>();
|
||||
const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
|
||||
|
||||
ctx.enableCache(file.path);
|
||||
|
||||
|
|
@ -225,8 +236,10 @@ export const processCalls = async (
|
|||
description: item.accessorType,
|
||||
},
|
||||
});
|
||||
ctx.symbols.add(file.path, item.propName, nodeId, 'Property',
|
||||
propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
|
||||
ctx.symbols.add(file.path, item.propName, nodeId, 'Property', {
|
||||
...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
|
||||
...(item.declaredType ? { declaredType: item.declaredType } : {}),
|
||||
});
|
||||
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
||||
graph.addRelationship({
|
||||
id: relId, sourceId: fileId, targetId: nodeId,
|
||||
|
|
@ -234,9 +247,9 @@ export const processCalls = async (
|
|||
});
|
||||
if (propEnclosingClassId) {
|
||||
graph.addRelationship({
|
||||
id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
|
||||
id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
|
||||
sourceId: propEnclosingClassId, targetId: nodeId,
|
||||
type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
||||
type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -255,10 +268,10 @@ export const processCalls = async (
|
|||
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
||||
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
||||
// Fall back to verified constructor bindings for return type inference
|
||||
if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
|
||||
if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
|
||||
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
||||
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
||||
receiverTypeName = lookupReceiverType(verifiedReceivers, funcName, receiverName);
|
||||
receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
|
||||
}
|
||||
// Fall back to class-as-receiver for static method calls (e.g. UserService.find_user()).
|
||||
// When the receiver name is not a variable in TypeEnv but resolves to a Class/Struct/Interface
|
||||
|
|
@ -271,32 +284,33 @@ export const processCalls = async (
|
|||
receiverTypeName = receiverName;
|
||||
}
|
||||
}
|
||||
// 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).
|
||||
// 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 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 && receiverIndex.size > 0) {
|
||||
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
||||
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
||||
baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName);
|
||||
currentType = lookupReceiverType(receiverIndex, 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) {
|
||||
receiverTypeName = walkMixedChain(extracted.chain, currentType, file.path, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -345,6 +359,7 @@ interface ResolveResult {
|
|||
nodeId: string;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
returnType?: string;
|
||||
}
|
||||
|
||||
const CALLABLE_SYMBOL_TYPES = new Set([
|
||||
|
|
@ -394,48 +409,9 @@ const toResolveResult = (
|
|||
nodeId: definition.nodeId,
|
||||
confidence: TIER_CONFIDENCE[tier],
|
||||
reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
|
||||
returnType: definition.returnType,
|
||||
});
|
||||
|
||||
/**
|
||||
* 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:
|
||||
|
|
@ -529,48 +505,139 @@ const receiverKey = (scope: string, varName: string): string =>
|
|||
`${scope}\0${varName}`;
|
||||
|
||||
/**
|
||||
* Look up a receiver type from a verified receiver map.
|
||||
* The map is keyed by `scope\0varName` (full scope with @startIndex).
|
||||
* Since the lookup side only has `funcName` (no startIndex), we scan for
|
||||
* all entries whose key starts with `funcName@` and has the matching varName.
|
||||
* If exactly one unique type is found, return it. If multiple distinct types
|
||||
* exist (true overload collision), return undefined (refuse to guess).
|
||||
* Falls back to the file-level scope key `\0varName` (empty funcName).
|
||||
* Pre-built secondary index for O(1) receiver type lookups.
|
||||
* Built once per file from the verified receiver map, keyed by funcName → varName.
|
||||
*/
|
||||
type ReceiverTypeEntry =
|
||||
| { readonly kind: 'resolved'; readonly value: string }
|
||||
| { readonly kind: 'ambiguous' };
|
||||
type ReceiverTypeIndex = Map<string, Map<string, ReceiverTypeEntry>>;
|
||||
|
||||
/**
|
||||
* Build a two-level secondary index from the verified receiver map.
|
||||
* The verified map is keyed by `scope\0varName` where scope is either
|
||||
* "funcName@startIndex" (inside a function) or "" (file level).
|
||||
* Index structure: Map<funcName, Map<varName, ReceiverTypeEntry>>
|
||||
*/
|
||||
const buildReceiverTypeIndex = (map: Map<string, string>): ReceiverTypeIndex => {
|
||||
const index: ReceiverTypeIndex = new Map();
|
||||
for (const [key, typeName] of map) {
|
||||
const nul = key.indexOf('\0');
|
||||
if (nul < 0) continue;
|
||||
const scope = key.slice(0, nul);
|
||||
const varName = key.slice(nul + 1);
|
||||
if (!varName) continue;
|
||||
if (scope !== '' && !scope.includes('@')) continue;
|
||||
const funcName = scope === '' ? '' : scope.slice(0, scope.indexOf('@'));
|
||||
|
||||
let varMap = index.get(funcName);
|
||||
if (!varMap) { varMap = new Map(); index.set(funcName, varMap); }
|
||||
|
||||
const existing = varMap.get(varName);
|
||||
if (existing === undefined) {
|
||||
varMap.set(varName, { kind: 'resolved', value: typeName });
|
||||
} else if (existing.kind === 'resolved' && existing.value !== typeName) {
|
||||
varMap.set(varName, { kind: 'ambiguous' });
|
||||
}
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
/**
|
||||
* O(1) receiver type lookup using the pre-built secondary index.
|
||||
* Returns the unique type name if unambiguous. Falls back to file-level scope.
|
||||
*/
|
||||
const lookupReceiverType = (
|
||||
map: Map<string, string>,
|
||||
index: ReceiverTypeIndex,
|
||||
funcName: string,
|
||||
varName: string,
|
||||
): string | undefined => {
|
||||
// Fast path: file-level scope (empty funcName — used as fallback)
|
||||
const fileLevelKey = receiverKey('', varName);
|
||||
|
||||
const prefix = `${funcName}@`;
|
||||
const suffix = `\0${varName}`;
|
||||
let found: string | undefined;
|
||||
let ambiguous = false;
|
||||
|
||||
for (const [key, value] of map) {
|
||||
if (key === fileLevelKey) continue; // handled separately below
|
||||
if (key.startsWith(prefix) && key.endsWith(suffix)) {
|
||||
// Verify the key is exactly "funcName@<digits>\0varName" with no extra chars.
|
||||
// The part between prefix and suffix should be the startIndex (digits only),
|
||||
// but we accept any non-empty segment to be forward-compatible.
|
||||
const middle = key.slice(prefix.length, key.length - suffix.length);
|
||||
if (middle.length === 0) continue; // malformed key — skip
|
||||
if (found === undefined) {
|
||||
found = value;
|
||||
} else if (found !== value) {
|
||||
ambiguous = true;
|
||||
break;
|
||||
}
|
||||
const funcBucket = index.get(funcName);
|
||||
if (funcBucket) {
|
||||
const entry = funcBucket.get(varName);
|
||||
if (entry?.kind === 'resolved') return entry.value;
|
||||
if (entry?.kind === 'ambiguous') {
|
||||
// Ambiguous in this function scope — try file-level fallback
|
||||
const fileEntry = index.get('')?.get(varName);
|
||||
return fileEntry?.kind === 'resolved' ? fileEntry.value : undefined;
|
||||
}
|
||||
}
|
||||
// Fallback: file-level scope (funcName "")
|
||||
if (funcName !== '') {
|
||||
const fileEntry = index.get('')?.get(varName);
|
||||
if (fileEntry?.kind === 'resolved') return fileEntry.value;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
if (!ambiguous && found !== undefined) return found;
|
||||
const resolveFieldAccessType = (
|
||||
receiverName: string,
|
||||
fieldName: string,
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
): string | undefined => {
|
||||
// Resolve the receiver's type to a class/struct nodeId
|
||||
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.type === 'Enum' || d.type === 'Record' || d.type === 'Impl',
|
||||
);
|
||||
if (!classDef) return undefined;
|
||||
|
||||
// Fallback: file-level scope (bindings outside any function)
|
||||
return map.get(fileLevelKey);
|
||||
const fieldDef = ctx.symbols.lookupFieldByOwner(classDef.nodeId, fieldName);
|
||||
if (!fieldDef?.declaredType) return undefined;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk a pre-built mixed chain of field/call steps, threading the current type
|
||||
* through each step and returning the final resolved type.
|
||||
*
|
||||
* Returns `undefined` if any step cannot be resolved (chain is broken).
|
||||
* The caller is responsible for seeding `startType` from its own context
|
||||
* (TypeEnv, constructor bindings, or static-class fallback).
|
||||
*/
|
||||
const walkMixedChain = (
|
||||
chain: MixedChainStep[],
|
||||
startType: string,
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
): string | undefined => {
|
||||
let currentType: string | undefined = startType;
|
||||
for (const step of chain) {
|
||||
if (!currentType) break;
|
||||
if (step.kind === 'field') {
|
||||
currentType = resolveFieldAccessType(currentType, step.name, filePath, ctx);
|
||||
} else {
|
||||
// Ruby/Python: property access is syntactically identical to method calls.
|
||||
// Try field resolution first — if the name is a known property with declaredType,
|
||||
// use that type directly. Otherwise fall back to method call resolution.
|
||||
const fieldType = resolveFieldAccessType(currentType, step.name, filePath, ctx);
|
||||
if (fieldType) {
|
||||
currentType = fieldType;
|
||||
continue;
|
||||
}
|
||||
const resolved = resolveCallTarget(
|
||||
{ calledName: step.name, callForm: 'member', receiverTypeName: currentType },
|
||||
filePath,
|
||||
ctx,
|
||||
);
|
||||
if (!resolved) {
|
||||
// Stdlib passthrough: unwrap(), clone(), etc. preserve the receiver type
|
||||
if (TYPE_PRESERVING_METHODS.has(step.name)) continue;
|
||||
currentType = undefined; break;
|
||||
}
|
||||
if (!resolved.returnType) { currentType = undefined; break; }
|
||||
const retType = extractReturnTypeName(resolved.returnType);
|
||||
if (!retType) { currentType = undefined; break; }
|
||||
currentType = retType;
|
||||
}
|
||||
}
|
||||
return currentType;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -587,12 +654,12 @@ export const processCallsFromExtracted = async (
|
|||
// Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
|
||||
// The scope dimension prevents collisions when two functions in the same file
|
||||
// have same-named locals pointing to different constructor types.
|
||||
const fileReceiverTypes = new Map<string, Map<string, string>>();
|
||||
const fileReceiverTypes = new Map<string, ReceiverTypeIndex>();
|
||||
if (constructorBindings) {
|
||||
for (const { filePath, bindings } of constructorBindings) {
|
||||
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
|
||||
if (verified.size > 0) {
|
||||
fileReceiverTypes.set(filePath, verified);
|
||||
fileReceiverTypes.set(filePath, buildReceiverTypeIndex(verified));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -638,29 +705,31 @@ export const processCallsFromExtracted = async (
|
|||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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);
|
||||
baseType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName);
|
||||
currentType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName);
|
||||
}
|
||||
const chainedType = resolveChainedReceiver(
|
||||
effectiveCall.receiverCallChain,
|
||||
baseType,
|
||||
effectiveCall.filePath,
|
||||
ctx,
|
||||
);
|
||||
if (chainedType) {
|
||||
effectiveCall = { ...effectiveCall, receiverTypeName: chainedType };
|
||||
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',
|
||||
)) {
|
||||
currentType = effectiveCall.receiverName;
|
||||
}
|
||||
}
|
||||
if (currentType) {
|
||||
const walkedType = walkMixedChain(
|
||||
effectiveCall.receiverMixedChain, currentType, effectiveCall.filePath, ctx,
|
||||
);
|
||||
if (walkedType) {
|
||||
effectiveCall = { ...effectiveCall, receiverTypeName: walkedType };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { KnowledgeGraph, GraphNode, GraphRelationship } from '../graph/types.js';
|
||||
import { KnowledgeGraph, GraphNode, GraphRelationship, type NodeLabel } from '../graph/types.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
|
||||
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 { 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';
|
||||
|
|
@ -81,6 +83,7 @@ const processParsingWithWorkers = async (
|
|||
symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
|
||||
parameterCount: sym.parameterCount,
|
||||
returnType: sym.returnType,
|
||||
declaredType: sym.declaredType,
|
||||
ownerId: sym.ownerId,
|
||||
});
|
||||
}
|
||||
|
|
@ -198,9 +201,24 @@ const processParsingSequential = async (
|
|||
if (!nameNode && !captureMap['definition.constructor']) return;
|
||||
const nodeName = nameNode ? nameNode.text : 'init';
|
||||
|
||||
let nodeLabel = 'CodeElement';
|
||||
let nodeLabel: 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';
|
||||
|
|
@ -275,9 +293,15 @@ const processParsingSequential = async (
|
|||
const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
|
||||
const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
|
||||
|
||||
// Extract declared type for Property nodes (field/property type annotations)
|
||||
const declaredType = (nodeLabel === 'Property' && definitionNode)
|
||||
? extractPropertyDeclaredType(definitionNode)
|
||||
: undefined;
|
||||
|
||||
symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
|
||||
parameterCount: methodSig?.parameterCount,
|
||||
returnType: methodSig?.returnType,
|
||||
declaredType,
|
||||
ownerId: enclosingClassId ?? undefined,
|
||||
});
|
||||
|
||||
|
|
@ -296,13 +320,14 @@ const processParsingSequential = async (
|
|||
|
||||
graph.addRelationship(relationship);
|
||||
|
||||
// ── HAS_METHOD: link method/constructor/property to enclosing class ──
|
||||
// ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
|
||||
if (enclosingClassId) {
|
||||
const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
|
||||
graph.addRelationship({
|
||||
id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
|
||||
id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
|
||||
sourceId: enclosingClassId,
|
||||
targetId: nodeId,
|
||||
type: 'HAS_METHOD',
|
||||
type: memberEdgeType,
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import type { NodeLabel } from '../graph/types.js';
|
||||
|
||||
export interface SymbolDefinition {
|
||||
nodeId: string;
|
||||
filePath: string;
|
||||
type: string; // 'Function', 'Class', etc.
|
||||
type: NodeLabel;
|
||||
parameterCount?: number;
|
||||
/** Raw return type text extracted from AST (e.g. 'User', 'Promise<User>') */
|
||||
returnType?: string;
|
||||
/** Links Method/Constructor to owning Class/Struct/Trait nodeId */
|
||||
/** Declared type for non-callable symbols — fields/properties (e.g. 'Address', 'List<User>') */
|
||||
declaredType?: string;
|
||||
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
|
||||
ownerId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -17,8 +21,8 @@ export interface SymbolTable {
|
|||
filePath: string,
|
||||
name: string,
|
||||
nodeId: string,
|
||||
type: string,
|
||||
metadata?: { parameterCount?: number; returnType?: string; ownerId?: string }
|
||||
type: NodeLabel,
|
||||
metadata?: { parameterCount?: number; returnType?: string; declaredType?: string; ownerId?: string }
|
||||
) => void;
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +49,14 @@ export interface SymbolTable {
|
|||
* Used by ReturnTypeLookup to resolve callee → return type.
|
||||
*/
|
||||
lookupFuzzyCallable: (name: string) => SymbolDefinition[];
|
||||
|
||||
|
||||
/**
|
||||
* Look up a field/property by its owning class nodeId and field name.
|
||||
* O(1) via dedicated eagerly-populated index keyed by `ownerNodeId\0fieldName`.
|
||||
* Returns undefined when no matching property exists or the owner is ambiguous.
|
||||
*/
|
||||
lookupFieldByOwner: (ownerNodeId: string, fieldName: string) => SymbolDefinition | undefined;
|
||||
|
||||
/**
|
||||
* Debugging: See how many symbols are tracked
|
||||
*/
|
||||
|
|
@ -71,14 +82,18 @@ export const createSymbolTable = (): SymbolTable => {
|
|||
// Only Function, Method, Constructor symbols are indexed.
|
||||
let callableIndex: Map<string, SymbolDefinition[]> | null = null;
|
||||
|
||||
// 4. Eagerly-populated Field/Property Index — keyed by "ownerNodeId\0fieldName".
|
||||
// Only Property symbols with ownerId and declaredType are indexed.
|
||||
const fieldByOwner = new Map<string, SymbolDefinition>();
|
||||
|
||||
const CALLABLE_TYPES = new Set(['Function', 'Method', 'Constructor']);
|
||||
|
||||
const add = (
|
||||
filePath: string,
|
||||
name: string,
|
||||
nodeId: string,
|
||||
type: string,
|
||||
metadata?: { parameterCount?: number; returnType?: string; ownerId?: string }
|
||||
type: NodeLabel,
|
||||
metadata?: { parameterCount?: number; returnType?: string; declaredType?: string; ownerId?: string }
|
||||
) => {
|
||||
const def: SymbolDefinition = {
|
||||
nodeId,
|
||||
|
|
@ -86,6 +101,7 @@ export const createSymbolTable = (): SymbolTable => {
|
|||
type,
|
||||
...(metadata?.parameterCount !== undefined ? { parameterCount: metadata.parameterCount } : {}),
|
||||
...(metadata?.returnType !== undefined ? { returnType: metadata.returnType } : {}),
|
||||
...(metadata?.declaredType !== undefined ? { declaredType: metadata.declaredType } : {}),
|
||||
...(metadata?.ownerId !== undefined ? { ownerId: metadata.ownerId } : {}),
|
||||
};
|
||||
|
||||
|
|
@ -95,14 +111,26 @@ export const createSymbolTable = (): SymbolTable => {
|
|||
}
|
||||
fileIndex.get(filePath)!.set(name, def);
|
||||
|
||||
// B. Add to Global Index (same object reference)
|
||||
// B. Properties go to fieldByOwner index only — skip globalIndex to prevent
|
||||
// namespace pollution for common names like 'id', 'name', 'type'.
|
||||
if (type === 'Property' && metadata?.ownerId) {
|
||||
if (metadata?.declaredType) {
|
||||
fieldByOwner.set(`${metadata.ownerId}\0${name}`, def);
|
||||
}
|
||||
// Still add to fileIndex above (for lookupExact), but skip globalIndex
|
||||
return;
|
||||
}
|
||||
|
||||
// C. Add to Global Index (same object reference)
|
||||
if (!globalIndex.has(name)) {
|
||||
globalIndex.set(name, []);
|
||||
}
|
||||
globalIndex.get(name)!.push(def);
|
||||
|
||||
// Invalidate the lazy callable index — it will be rebuilt on next use
|
||||
callableIndex = null;
|
||||
// D. Invalidate the lazy callable index only when adding callable types
|
||||
if (CALLABLE_TYPES.has(type)) {
|
||||
callableIndex = null;
|
||||
}
|
||||
};
|
||||
|
||||
const lookupExact = (filePath: string, name: string): string | undefined => {
|
||||
|
|
@ -129,6 +157,10 @@ export const createSymbolTable = (): SymbolTable => {
|
|||
return callableIndex.get(name) ?? [];
|
||||
};
|
||||
|
||||
const lookupFieldByOwner = (ownerNodeId: string, fieldName: string): SymbolDefinition | undefined => {
|
||||
return fieldByOwner.get(`${ownerNodeId}\0${fieldName}`);
|
||||
};
|
||||
|
||||
const getStats = () => ({
|
||||
fileCount: fileIndex.size,
|
||||
globalSymbolCount: globalIndex.size
|
||||
|
|
@ -138,7 +170,8 @@ export const createSymbolTable = (): SymbolTable => {
|
|||
fileIndex.clear();
|
||||
globalIndex.clear();
|
||||
callableIndex = null;
|
||||
fieldByOwner.clear();
|
||||
};
|
||||
|
||||
return { add, lookupExact, lookupExactFull, lookupFuzzy, lookupFuzzyCallable, getStats, clear };
|
||||
return { add, lookupExact, lookupExactFull, lookupFuzzy, lookupFuzzyCallable, lookupFieldByOwner, getStats, clear };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -62,6 +62,19 @@ export const TYPESCRIPT_QUERIES = `
|
|||
(new_expression
|
||||
constructor: (identifier) @call.name) @call
|
||||
|
||||
; Class properties — public_field_definition covers most TS class fields
|
||||
(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
|
||||
|
|
@ -128,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
|
||||
|
|
@ -160,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
|
||||
|
|
@ -179,6 +204,11 @@ export const JAVA_QUERIES = `
|
|||
(method_declaration name: (identifier) @name) @definition.method
|
||||
(constructor_declaration name: (identifier) @name) @definition.constructor
|
||||
|
||||
; Fields — typed field declarations inside class bodies
|
||||
(field_declaration
|
||||
declarator: (variable_declarator
|
||||
name: (identifier) @name)) @definition.property
|
||||
|
||||
; Imports - capture any import declaration child as source
|
||||
(import_declaration (_) @import.source) @import
|
||||
|
||||
|
|
@ -243,6 +273,11 @@ export const GO_QUERIES = `
|
|||
(import_declaration (import_spec path: (interpreted_string_literal) @import.source)) @import
|
||||
(import_declaration (import_spec_list (import_spec path: (interpreted_string_literal) @import.source))) @import
|
||||
|
||||
; Struct fields — named field declarations inside struct types
|
||||
(field_declaration_list
|
||||
(field_declaration
|
||||
name: (field_identifier) @name) @definition.property)
|
||||
|
||||
; Struct embedding (anonymous fields = inheritance)
|
||||
(type_declaration
|
||||
(type_spec
|
||||
|
|
@ -299,6 +334,21 @@ 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
|
||||
|
||||
; Pointer member fields (Address* address;)
|
||||
(field_declaration
|
||||
declarator: (pointer_declarator
|
||||
declarator: (field_identifier) @name)) @definition.property
|
||||
|
||||
; Reference member fields (Address& address;)
|
||||
(field_declaration
|
||||
declarator: (reference_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
|
||||
|
||||
|
|
@ -414,6 +464,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
|
||||
|
|
@ -457,6 +512,13 @@ export const PHP_QUERIES = `
|
|||
(variable_name
|
||||
(name) @name))) @definition.property
|
||||
|
||||
; Constructor property promotion (PHP 8.0+: public Address $address in __construct)
|
||||
(method_declaration
|
||||
parameters: (formal_parameters
|
||||
(property_promotion_parameter
|
||||
name: (variable_name
|
||||
(name) @name)))) @definition.property
|
||||
|
||||
; ── Imports: use statements ──────────────────────────────────────────────────
|
||||
; Simple: use App\\Models\\User;
|
||||
(namespace_use_declaration
|
||||
|
|
@ -582,6 +644,12 @@ export const KOTLIN_QUERIES = `
|
|||
(variable_declaration
|
||||
(simple_identifier) @name)) @definition.property
|
||||
|
||||
; Primary constructor val/var parameters (data class, value class, regular class)
|
||||
; binding_pattern_kind contains "val" or "var" — without it, the param is not a property
|
||||
(class_parameter
|
||||
(binding_pattern_kind)
|
||||
(simple_identifier) @name) @definition.property
|
||||
|
||||
; ── Enum entries ─────────────────────────────────────────────────────────
|
||||
(enum_entry
|
||||
(simple_identifier) @name) @definition.enum
|
||||
|
|
|
|||
|
|
@ -439,7 +439,9 @@ export const buildTypeEnv = (
|
|||
let typeNode = node.childForFieldName('type');
|
||||
if (typeNode) {
|
||||
const nameNode = node.childForFieldName('name')
|
||||
?? node.childForFieldName('pattern');
|
||||
?? node.childForFieldName('pattern')
|
||||
// Python typed_parameter: name is a positional child (identifier), not a named field
|
||||
?? (node.firstNamedChild?.type === 'identifier' ? node.firstNamedChild : null);
|
||||
if (nameNode) {
|
||||
const varName = extractVarName(nameNode);
|
||||
if (varName && !declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
|
|||
} else {
|
||||
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
||||
typeNode = node.childForFieldName('type');
|
||||
// Python typed_parameter: name is a positional child (identifier), not a named field
|
||||
if (!nameNode && node.type === 'typed_parameter') {
|
||||
nameNode = node.firstNamedChild?.type === 'identifier' ? node.firstNamedChild : null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nameNode || !typeNode) return;
|
||||
|
|
|
|||
|
|
@ -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: SyntaxNode | null): 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',
|
||||
|
|
@ -318,6 +320,21 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
|
|||
}
|
||||
}
|
||||
}
|
||||
// Go: type_declaration wrapping a struct_type (type User struct { ... })
|
||||
// field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
|
||||
if (current.type === 'type_declaration') {
|
||||
const typeSpec = current.children?.find((c: any) => c.type === 'type_spec');
|
||||
if (typeSpec) {
|
||||
const typeBody = typeSpec.childForFieldName?.('type');
|
||||
if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
|
||||
const nameNode = typeSpec.childForFieldName?.('name');
|
||||
if (nameNode) {
|
||||
const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
|
||||
return generateId(label, `${filePath}:${nameNode.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
||||
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
|
||||
if (current.type === 'impl_item') {
|
||||
|
|
@ -1156,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,6 +48,8 @@ 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 { extractPropertyDeclaredType } from '../type-extractors/shared.js';
|
||||
import type { NodeLabel } from '../../graph/types.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types for serializable results
|
||||
|
|
@ -75,7 +77,7 @@ interface ParsedRelationship {
|
|||
id: string;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
type: 'DEFINES' | 'HAS_METHOD';
|
||||
type: 'DEFINES' | 'HAS_METHOD' | 'HAS_PROPERTY';
|
||||
confidence: number;
|
||||
reason: string;
|
||||
}
|
||||
|
|
@ -84,9 +86,10 @@ interface ParsedSymbol {
|
|||
filePath: string;
|
||||
name: string;
|
||||
nodeId: string;
|
||||
type: string;
|
||||
type: NodeLabel;
|
||||
parameterCount?: number;
|
||||
returnType?: string;
|
||||
declaredType?: string;
|
||||
ownerId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -111,13 +114,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[];
|
||||
receiverMixedChain?: MixedChainStep[];
|
||||
}
|
||||
|
||||
export interface ExtractedHeritage {
|
||||
|
|
@ -233,7 +237,7 @@ const findEnclosingFunctionId = (node: any, filePath: string): string | null =>
|
|||
// Label detection from capture map
|
||||
// ============================================================================
|
||||
|
||||
const getLabelFromCaptures = (captureMap: Record<string, any>): string | null => {
|
||||
const getLabelFromCaptures = (captureMap: Record<string, any>): NodeLabel | null => {
|
||||
// Skip imports (handled separately) and calls
|
||||
if (captureMap['import'] || captureMap['call']) return null;
|
||||
if (!captureMap['name']) return null;
|
||||
|
|
@ -965,6 +969,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}`);
|
||||
|
|
@ -978,10 +983,10 @@ const processFileGroup = (
|
|||
});
|
||||
if (propEnclosingClassId) {
|
||||
result.relationships.push({
|
||||
id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
|
||||
id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
|
||||
sourceId: propEnclosingClassId,
|
||||
targetId: nodeId,
|
||||
type: 'HAS_METHOD',
|
||||
type: 'HAS_PROPERTY',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
|
|
@ -1000,27 +1005,20 @@ 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;
|
||||
let receiverMixedChain: MixedChainStep[] | 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.
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -1036,7 +1034,7 @@ const processFileGroup = (
|
|||
...(callForm !== undefined ? { callForm } : {}),
|
||||
...(receiverName !== undefined ? { receiverName } : {}),
|
||||
...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
|
||||
...(receiverCallChain !== undefined ? { receiverCallChain } : {}),
|
||||
...(receiverMixedChain !== undefined ? { receiverMixedChain } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1086,6 +1084,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;
|
||||
|
|
@ -1109,6 +1124,7 @@ const processFileGroup = (
|
|||
|
||||
let parameterCount: number | undefined;
|
||||
let returnType: string | undefined;
|
||||
let declaredType: string | undefined;
|
||||
if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
|
||||
const sig = extractMethodSignature(definitionNode);
|
||||
parameterCount = sig.parameterCount;
|
||||
|
|
@ -1123,6 +1139,10 @@ const processFileGroup = (
|
|||
if (docReturn) returnType = docReturn;
|
||||
}
|
||||
}
|
||||
} else if (nodeLabel === 'Property' && definitionNode) {
|
||||
// Extract the declared type for property/field nodes.
|
||||
// Walk the definition node for type annotation children.
|
||||
declaredType = extractPropertyDeclaredType(definitionNode);
|
||||
}
|
||||
|
||||
result.nodes.push({
|
||||
|
|
@ -1157,6 +1177,7 @@ const processFileGroup = (
|
|||
type: nodeLabel,
|
||||
...(parameterCount !== undefined ? { parameterCount } : {}),
|
||||
...(returnType !== undefined ? { returnType } : {}),
|
||||
...(declaredType !== undefined ? { declaredType } : {}),
|
||||
...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
|
||||
});
|
||||
|
||||
|
|
@ -1171,13 +1192,14 @@ const processFileGroup = (
|
|||
reason: '',
|
||||
});
|
||||
|
||||
// ── HAS_METHOD: link method/constructor/property to enclosing class ──
|
||||
// ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
|
||||
if (enclosingClassId) {
|
||||
const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
|
||||
result.relationships.push({
|
||||
id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
|
||||
id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
|
||||
sourceId: enclosingClassId,
|
||||
targetId: nodeId,
|
||||
type: 'HAS_METHOD',
|
||||
type: memberEdgeType,
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export type NodeTableName = typeof NODE_TABLES[number];
|
|||
export const REL_TABLE_NAME = 'CodeRelation';
|
||||
|
||||
// Valid relation types
|
||||
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'] as const;
|
||||
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'] as const;
|
||||
export type RelType = typeof REL_TYPES[number];
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const VALID_NODE_LABELS = new Set([
|
|||
]);
|
||||
|
||||
/** Valid relation types for impact analysis filtering */
|
||||
export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']);
|
||||
export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES']);
|
||||
|
||||
/** Regex to detect write operations in user-supplied Cypher queries */
|
||||
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
|
||||
|
|
@ -898,7 +898,7 @@ export class LocalBackend {
|
|||
// Categorized incoming refs
|
||||
const incomingRows = await executeParameterized(repo.id, `
|
||||
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES']
|
||||
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
||||
LIMIT 30
|
||||
`, { symId });
|
||||
|
|
@ -906,7 +906,7 @@ export class LocalBackend {
|
|||
// Categorized outgoing refs
|
||||
const outgoingRows = await executeParameterized(repo.id, `
|
||||
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES']
|
||||
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
||||
LIMIT 30
|
||||
`, { symId });
|
||||
|
|
|
|||
|
|
@ -328,6 +328,9 @@ relationships:
|
|||
- IMPORTS: Module imports
|
||||
- EXTENDS: Class inheritance
|
||||
- IMPLEMENTS: Interface implementation
|
||||
- HAS_METHOD: Class/Struct/Interface owns a Method
|
||||
- HAS_PROPERTY: Class/Struct/Interface owns a Property (field)
|
||||
- OVERRIDES: Method overrides another Method (MRO)
|
||||
- MEMBER_OF: Symbol belongs to community
|
||||
- STEP_IN_PROCESS: Symbol is step N in process
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ SCHEMA:
|
|||
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
|
||||
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
|
||||
- All edges via single CodeRelation table with 'type' property
|
||||
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
|
||||
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
|
||||
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
|
||||
|
||||
EXAMPLES:
|
||||
|
|
@ -94,6 +94,9 @@ EXAMPLES:
|
|||
• Find all methods of a class:
|
||||
MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
|
||||
|
||||
• Find all properties of a class:
|
||||
MATCH (c:Class {name: "User"})-[r:CodeRelation {type: 'HAS_PROPERTY'}]->(p:Property) RETURN p.name, p.description
|
||||
|
||||
• Find method overrides (MRO resolution):
|
||||
MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
|
||||
|
||||
|
|
@ -119,7 +122,7 @@ TIPS:
|
|||
{
|
||||
name: 'context',
|
||||
description: `360-degree view of a single code symbol.
|
||||
Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location.
|
||||
Shows categorized incoming/outgoing references (calls, imports, extends, implements, methods, properties, overrides), process participation, and file location.
|
||||
|
||||
WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
|
||||
AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
|
||||
|
|
@ -200,7 +203,9 @@ Depth groups:
|
|||
- d=2: LIKELY AFFECTED (indirect)
|
||||
- d=3: MAY NEED TESTING (transitive)
|
||||
|
||||
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES
|
||||
TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes.
|
||||
|
||||
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES
|
||||
Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
|
|
@ -208,7 +213,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|||
target: { type: 'string', description: 'Name of function, class, or file to analyze' },
|
||||
direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
|
||||
maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
|
||||
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' },
|
||||
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES (default: usage-based)' },
|
||||
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
|
||||
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
|
||||
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
21
gitnexus/test/fixtures/lang-resolution/cpp-pointer-ref-fields/models.h
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/cpp-pointer-ref-fields/models.h
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
class Address {
|
||||
public:
|
||||
std::string city;
|
||||
|
||||
void save() {
|
||||
// persist address
|
||||
}
|
||||
};
|
||||
|
||||
class User {
|
||||
public:
|
||||
Address* address; // raw pointer member field
|
||||
Address& ref_address; // reference member field
|
||||
std::string name;
|
||||
|
||||
std::string greet() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-pointer-ref-fields/service.cpp
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-pointer-ref-fields/service.cpp
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "models.h"
|
||||
|
||||
void processUser(User user) {
|
||||
// Pointer member field access: user.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();
|
||||
}
|
||||
}
|
||||
22
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Models.cs
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Models.cs
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace FieldTypes;
|
||||
|
||||
public class Address
|
||||
{
|
||||
public string City { 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;
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Service.cs
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/csharp-field-types/Service.cs
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace FieldTypes;
|
||||
|
||||
public class Service
|
||||
{
|
||||
public static void ProcessUser(User user)
|
||||
{
|
||||
// Field-access chain: user.Address → Address, then .Save() → Address#Save
|
||||
user.Address.Save();
|
||||
}
|
||||
}
|
||||
24
gitnexus/test/fixtures/lang-resolution/field-types/models.ts
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/field-types/models.ts
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export class Address {
|
||||
city: string;
|
||||
|
||||
save(): void {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
export class User {
|
||||
name: string;
|
||||
address: Address;
|
||||
|
||||
greet(): string {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class Config {
|
||||
static DEFAULT: Config = new Config();
|
||||
|
||||
validate(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/field-types/service.ts
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/field-types/service.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { User, Config } from './models';
|
||||
|
||||
function processUser(user: User) {
|
||||
// Field-access chain: user.address resolves to Address, then .save() resolves to Address#save
|
||||
user.address.save();
|
||||
}
|
||||
|
||||
function validateConfig() {
|
||||
// Static field access: Config.DEFAULT resolves to Config, then .validate() resolves to Config#validate
|
||||
Config.DEFAULT.validate();
|
||||
}
|
||||
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
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/go-field-types/cmd/main.go
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/go-field-types/cmd/main.go
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package main
|
||||
|
||||
import "example.com/go-field-types/models"
|
||||
|
||||
func processUser(user models.User) {
|
||||
// Field-access chain: user.Address → Address, then .Save() → Address#Save
|
||||
user.Address.Save()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-field-types/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-field-types/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/go-field-types
|
||||
|
||||
go 1.21
|
||||
18
gitnexus/test/fixtures/lang-resolution/go-field-types/models/models.go
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/go-field-types/models/models.go
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package models
|
||||
|
||||
type Address struct {
|
||||
City 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/go-mixed-chain/cmd/main.go
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/go-mixed-chain/cmd/main.go
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package main
|
||||
|
||||
import "example.com/go-mixed-chain/models"
|
||||
|
||||
func processWithService(svc *models.UserService) {
|
||||
svc.GetUser().Address.Save()
|
||||
}
|
||||
|
||||
func processWithUser(user *models.User) {
|
||||
user.GetAddress().City.GetName()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-mixed-chain/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-mixed-chain/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/go-mixed-chain
|
||||
|
||||
go 1.21
|
||||
32
gitnexus/test/fixtures/lang-resolution/go-mixed-chain/models/models.go
vendored
Normal file
32
gitnexus/test/fixtures/lang-resolution/go-mixed-chain/models/models.go
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package models
|
||||
|
||||
type City struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c *City) GetName() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
City City
|
||||
Street string
|
||||
}
|
||||
|
||||
func (a *Address) Save() {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
func (u *User) GetAddress() *Address {
|
||||
return &u.Address
|
||||
}
|
||||
|
||||
type UserService struct{}
|
||||
|
||||
func (s *UserService) GetUser() *User {
|
||||
return &User{}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-field-types/App.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-field-types/App.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import models.User;
|
||||
|
||||
public class App {
|
||||
public static void processUser(User user) {
|
||||
// Field-access chain: user.address → Address, then .save() → Address#save
|
||||
user.address.save();
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-field-types/models/Address.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-field-types/models/Address.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models;
|
||||
|
||||
public class Address {
|
||||
public String city;
|
||||
|
||||
public void save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-field-types/models/User.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-field-types/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;
|
||||
}
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/App.java
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/App.java
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import services.UserService;
|
||||
import models.User;
|
||||
|
||||
public class App {
|
||||
public static void processWithService(UserService svc) {
|
||||
svc.getUser().address.save();
|
||||
}
|
||||
|
||||
public static void processWithUser(User user) {
|
||||
user.getAddress().city.getName();
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/models/Address.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/models/Address.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models;
|
||||
|
||||
public class Address {
|
||||
public City city;
|
||||
|
||||
public void save() {
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/models/City.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/models/City.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models;
|
||||
|
||||
public class City {
|
||||
public String getName() {
|
||||
return "city";
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/models/User.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public Address address;
|
||||
|
||||
public Address getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/services/UserService.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-mixed-chain/services/UserService.java
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package services;
|
||||
|
||||
import models.User;
|
||||
|
||||
public class UserService {
|
||||
public User getUser() {
|
||||
return new User();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/kotlin-data-class-fields/Models.kt
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/kotlin-data-class-fields/Models.kt
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class Address {
|
||||
var city: String = ""
|
||||
|
||||
fun save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
data class User(
|
||||
val name: String,
|
||||
val address: Address,
|
||||
val age: Int
|
||||
)
|
||||
4
gitnexus/test/fixtures/lang-resolution/kotlin-data-class-fields/Service.kt
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/kotlin-data-class-fields/Service.kt
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fun processUser(user: User) {
|
||||
// Field-access chain: user.address → Address, then .save() → Address#save
|
||||
user.address.save()
|
||||
}
|
||||
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()
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Models.kt
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Models.kt
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class Address {
|
||||
var city: String = ""
|
||||
|
||||
fun save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
var name: String = ""
|
||||
var address: Address = Address()
|
||||
|
||||
fun greet(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Service.kt
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/kotlin-field-types/Service.kt
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
fun processUser(user: User) {
|
||||
// Field-access chain: user.address → Address, then .save() → Address#save
|
||||
user.address.save()
|
||||
}
|
||||
20
gitnexus/test/fixtures/lang-resolution/php-constructor-promotion-fields/Models.php
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/php-constructor-promotion-fields/Models.php
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
class Address {
|
||||
public string $city;
|
||||
|
||||
public function save(): void {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public Address $address,
|
||||
) {}
|
||||
|
||||
public function greet(): string {
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-constructor-promotion-fields/Service.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-constructor-promotion-fields/Service.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
class Service {
|
||||
public function processUser(User $user): void {
|
||||
// Field-access chain: $user->address → Address, then ->save() → Address#save
|
||||
$user->address->save();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
22
gitnexus/test/fixtures/lang-resolution/php-field-types/Models.php
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/php-field-types/Models.php
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
class Address {
|
||||
/** @var string */
|
||||
public string $city;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/php-field-types/Service.php
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/php-field-types/Service.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
class Service {
|
||||
public function processUser(User $user): void {
|
||||
// Field-access chain: $user->address → Address, then ->save() → Address#save
|
||||
$user->address->save();
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-field-type-disambig/address.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-field-type-disambig/address.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class Address:
|
||||
city: str
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
6
gitnexus/test/fixtures/lang-resolution/python-field-type-disambig/service.py
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/python-field-type-disambig/service.py
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from user import User
|
||||
|
||||
def process_user(user: User):
|
||||
# Field-access chain: user.address → Address, then .save() must resolve
|
||||
# to Address#save (NOT User#save) — only lookupFieldByOwner can disambiguate.
|
||||
user.address.save()
|
||||
8
gitnexus/test/fixtures/lang-resolution/python-field-type-disambig/user.py
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/python-field-type-disambig/user.py
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from address import Address
|
||||
|
||||
class User:
|
||||
name: str
|
||||
address: Address
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
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()
|
||||
8
gitnexus/test/fixtures/lang-resolution/ruby-field-type-disambig/address.rb
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ruby-field-type-disambig/address.rb
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class Address
|
||||
# @return [String]
|
||||
attr_accessor :city
|
||||
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
8
gitnexus/test/fixtures/lang-resolution/ruby-field-type-disambig/service.rb
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/ruby-field-type-disambig/service.rb
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
require_relative 'user'
|
||||
|
||||
# @param user [User]
|
||||
def process_user(user)
|
||||
# Field-access chain: user.address → Address, then .save → Address#save
|
||||
# Both User and Address have save — only lookupFieldByOwner can disambiguate.
|
||||
user.address.save
|
||||
end
|
||||
13
gitnexus/test/fixtures/lang-resolution/ruby-field-type-disambig/user.rb
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/ruby-field-type-disambig/user.rb
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
require_relative 'address'
|
||||
|
||||
class User
|
||||
# @return [String]
|
||||
attr_accessor :name
|
||||
|
||||
# @return [Address]
|
||||
attr_accessor :address
|
||||
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
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
|
||||
25
gitnexus/test/fixtures/lang-resolution/rust-deep-field-chain/models.rs
vendored
Normal file
25
gitnexus/test/fixtures/lang-resolution/rust-deep-field-chain/models.rs
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
pub struct City {
|
||||
pub zip_code: String,
|
||||
}
|
||||
|
||||
impl City {
|
||||
pub fn get_name(&self) -> &str {
|
||||
"city"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Address {
|
||||
pub city: City,
|
||||
pub street: String,
|
||||
}
|
||||
|
||||
impl Address {
|
||||
pub fn save(&self) {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub address: Address,
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/rust-deep-field-chain/service.rs
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/rust-deep-field-chain/service.rs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use crate::models::{User, Address, City};
|
||||
|
||||
fn process_user(user: &User) {
|
||||
user.address.save();
|
||||
user.address.city.get_name();
|
||||
}
|
||||
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,126 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pointer and reference member fields (Address* address; Address& ref_address;)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ pointer/reference member field capture', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-pointer-ref-fields'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for pointer and reference member fields', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('ref_address');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('city');
|
||||
});
|
||||
|
||||
it('emits HAS_PROPERTY edges for pointer/reference fields', () => {
|
||||
const propEdges = getRelationships(result, 'HAS_PROPERTY');
|
||||
expect(edgeSet(propEdges)).toContain('User → address');
|
||||
expect(edgeSet(propEdges)).toContain('User → ref_address');
|
||||
expect(edgeSet(propEdges)).toContain('User → name');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -940,3 +940,127 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mixed field+call chain resolution (Go)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Mixed field+call chain resolution (Go)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'go-mixed-chain'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects structs: Address, City, User, UserService', () => {
|
||||
expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'City', 'User', 'UserService']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for mixed-chain fields', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1068,3 +1068,132 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mixed field+call chain resolution (Java)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Mixed field+call chain resolution (Java)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'java-mixed-chain'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, App, City, User, UserService', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'App', 'City', 'User', 'UserService']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for mixed-chain fields', () => {
|
||||
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('Address');
|
||||
});
|
||||
|
||||
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('City');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,135 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kotlin data class primary constructor val/var properties
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Kotlin data class primary constructor property capture', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'kotlin-data-class-fields'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for data class val parameters', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('age');
|
||||
});
|
||||
|
||||
it('emits HAS_PROPERTY edges for primary constructor properties', () => {
|
||||
const propEdges = getRelationships(result, 'HAS_PROPERTY');
|
||||
expect(edgeSet(propEdges)).toContain('User → name');
|
||||
expect(edgeSet(propEdges)).toContain('User → address');
|
||||
expect(edgeSet(propEdges)).toContain('User → age');
|
||||
});
|
||||
|
||||
it('resolves user.address.save() → Address#save via data class 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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1161,3 +1161,128 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PHP 8.0+ constructor promotion as property declarations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('PHP constructor promotion property capture', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'php-constructor-promotion-fields'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects classes: Address, Service, User', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Service', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for promoted constructor parameters', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
expect(properties).toContain('name');
|
||||
expect(properties).toContain('address');
|
||||
});
|
||||
|
||||
it('emits HAS_PROPERTY edges for promoted parameters', () => {
|
||||
const propEdges = getRelationships(result, 'HAS_PROPERTY');
|
||||
expect(edgeSet(propEdges)).toContain('User → name');
|
||||
expect(edgeSet(propEdges)).toContain('User → address');
|
||||
});
|
||||
|
||||
it('resolves $user->address->save() → Address#save via promoted 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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1281,3 +1281,77 @@ describe('Python enumerate() for-loop resolution', () => {
|
|||
expect(userSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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');
|
||||
});
|
||||
|
||||
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 === 'process_user' && e.targetFilePath.includes('models'),
|
||||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 8: Field type disambiguation — both User and Address have save()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type disambiguation (Python)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'python-field-type-disambig'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects both User#save and Address#save', () => {
|
||||
const methods = getNodesByLabel(result, 'Function');
|
||||
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 === 'process_user',
|
||||
);
|
||||
expect(saveCalls.length).toBe(1);
|
||||
expect(saveCalls[0].targetFilePath).toContain('address');
|
||||
expect(saveCalls[0].targetFilePath).not.toContain('user');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -124,15 +124,15 @@ describe('Ruby require_relative, heritage & property resolution', () => {
|
|||
expect(props).toContain('email');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD from User to attr_reader :name', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const edge = hasMethod.find(e => e.source === 'User' && e.target === 'name');
|
||||
it('emits HAS_PROPERTY from User to attr_reader :name', () => {
|
||||
const hasProperty = getRelationships(result, 'HAS_PROPERTY');
|
||||
const edge = hasProperty.find(e => e.source === 'User' && e.target === 'name');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD from BaseModel to attr_accessor :id', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const edge = hasMethod.find(e => e.source === 'BaseModel' && e.target === 'id');
|
||||
it('emits HAS_PROPERTY from BaseModel to attr_accessor :id', () => {
|
||||
const hasProperty = getRelationships(result, 'HAS_PROPERTY');
|
||||
const edge = hasProperty.find(e => e.source === 'BaseModel' && e.target === 'id');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
|
|
@ -846,3 +846,77 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 8: Field type disambiguation — both User and Address have save()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Field type disambiguation (Ruby)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-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 === 'process_user',
|
||||
);
|
||||
expect(saveCalls.length).toBe(1);
|
||||
expect(saveCalls[0].targetFilePath).toContain('address');
|
||||
expect(saveCalls[0].targetFilePath).not.toContain('user');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,88 @@ 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);
|
||||
});
|
||||
|
||||
it('resolves user.address.save() → Address#save via field type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(
|
||||
e => e.target === 'save' && e.source === 'process_user',
|
||||
);
|
||||
expect(saveCalls.length).toBe(1);
|
||||
expect(saveCalls[0].targetFilePath).toContain('models');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 8B: Deep field chain resolution (3-level)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Deep field chain resolution (Rust)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-deep-field-chain'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects structs: Address, City, User', () => {
|
||||
expect(getNodesByLabel(result, 'Struct')).toEqual(['Address', 'City', 'User']);
|
||||
});
|
||||
|
||||
it('detects Property nodes for Rust struct fields', () => {
|
||||
const properties = getNodesByLabel(result, 'Property');
|
||||
expect(properties).toContain('address');
|
||||
expect(properties).toContain('city');
|
||||
expect(properties).toContain('zip_code');
|
||||
});
|
||||
|
||||
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 === 'process_user');
|
||||
const addressSave = saveCalls.find(e => e.targetFilePath.includes('models'));
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves 3-level chain: user.address.city.get_name() → City#get_name', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const getNameCalls = calls.filter(e => e.target === 'get_name' && e.source === 'process_user');
|
||||
const cityGetName = getNameCalls.find(e => e.targetFilePath.includes('models'));
|
||||
expect(cityGetName).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ describe('TypeScript heritage resolution', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('emits HAS_PROPERTY edge for class fields', () => {
|
||||
const hasProperty = getRelationships(result, 'HAS_PROPERTY');
|
||||
expect(hasProperty.length).toBe(1);
|
||||
expect(edgeSet(hasProperty)).toEqual(['BaseService → name']);
|
||||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
|
|
@ -1692,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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -292,19 +292,19 @@ describe('computeMRO', () => {
|
|||
addExtends(graph, 'Child', 'ParentA');
|
||||
addExtends(graph, 'Child', 'ParentB');
|
||||
|
||||
// Add Property nodes (same name 'name') to both parents via HAS_METHOD
|
||||
// Add Property nodes (same name 'name') to both parents via HAS_PROPERTY
|
||||
const propA = generateId('Property', 'ParentA.name');
|
||||
graph.addNode({ id: propA, label: 'Property', properties: { name: 'name', filePath: 'src/ParentA.ts' } });
|
||||
graph.addRelationship({
|
||||
id: generateId('HAS_METHOD', `${parentA}->${propA}`),
|
||||
sourceId: parentA, targetId: propA, type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
||||
id: generateId('HAS_PROPERTY', `${parentA}->${propA}`),
|
||||
sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
||||
});
|
||||
|
||||
const propB = generateId('Property', 'ParentB.name');
|
||||
graph.addNode({ id: propB, label: 'Property', properties: { name: 'name', filePath: 'src/ParentB.ts' } });
|
||||
graph.addRelationship({
|
||||
id: generateId('HAS_METHOD', `${parentB}->${propB}`),
|
||||
sourceId: parentB, targetId: propB, type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
||||
id: generateId('HAS_PROPERTY', `${parentB}->${propB}`),
|
||||
sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
||||
});
|
||||
|
||||
const result = computeMRO(graph);
|
||||
|
|
@ -328,19 +328,19 @@ describe('computeMRO', () => {
|
|||
const methodA = addMethod(graph, 'PA', 'doWork');
|
||||
addMethod(graph, 'PB', 'doWork');
|
||||
|
||||
// Property collision (should NOT trigger OVERRIDES)
|
||||
// Property collision (should NOT trigger OVERRIDES — properties use HAS_PROPERTY, not HAS_METHOD)
|
||||
const propA = generateId('Property', 'PA.id');
|
||||
graph.addNode({ id: propA, label: 'Property', properties: { name: 'id', filePath: 'src/PA.ts' } });
|
||||
graph.addRelationship({
|
||||
id: generateId('HAS_METHOD', `${parentA}->${propA}`),
|
||||
sourceId: parentA, targetId: propA, type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
||||
id: generateId('HAS_PROPERTY', `${parentA}->${propA}`),
|
||||
sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
||||
});
|
||||
|
||||
const propB = generateId('Property', 'PB.id');
|
||||
graph.addNode({ id: propB, label: 'Property', properties: { name: 'id', filePath: 'src/PB.ts' } });
|
||||
graph.addRelationship({
|
||||
id: generateId('HAS_METHOD', `${parentB}->${propB}`),
|
||||
sourceId: parentB, targetId: propB, type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
||||
id: generateId('HAS_PROPERTY', `${parentB}->${propB}`),
|
||||
sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '',
|
||||
});
|
||||
|
||||
const result = computeMRO(graph);
|
||||
|
|
|
|||
|
|
@ -122,11 +122,11 @@ describe('LadybugDB Schema', () => {
|
|||
|
||||
it('has all FROM/TO pairs needed for HAS_METHOD edges', () => {
|
||||
// HAS_METHOD sources: Class, Interface, Struct, Trait, Impl, Record
|
||||
// HAS_METHOD targets: Method, Constructor, Property
|
||||
// HAS_METHOD targets: Method, Constructor (Property is now HAS_PROPERTY)
|
||||
const sources = ['Class', 'Interface'];
|
||||
const backtickSources = ['Struct', 'Trait', 'Impl', 'Record'];
|
||||
const targets = ['Method'];
|
||||
const backtickTargets = ['Constructor', 'Property'];
|
||||
const backtickTargets = ['Constructor'];
|
||||
|
||||
// Non-backtick source → non-backtick target
|
||||
for (const src of sources) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue