mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
* feat: Phase 6 type resolution — pattern matching, for-loop Tier 1c, coverage completion
- Add patternBindingNodeTypes gate to LanguageTypeConfig for 50% perf improvement
- Expand ForLoopExtractor signature with optional declarationTypeNodes + scope
- Add extractElementTypeFromString shared utility for container type parsing
- Python match/case: extractPatternBinding for `case User() as u:` pattern
- C# refactor: move is_pattern_expression from extractDeclaration to extractPatternBinding
- Ruby: add extractPendingAssignment for assignment chain propagation
- TS/JS: add for-loop Tier 1c for `for (const user of users)` with User[] inference
- Python: add for-loop Tier 1c for `for user in users:` with type annotation inference
- Go: add for-loop Tier 1c for `for _, user := range users` with []User inference
- Fix 'Property' as any stale cast in call-processor.ts
- Add dual return-type string length cap (2048 pre-cap, 512 post-cap)
- Add chain call integration tests for C#, Go, Rust, Python, JS, C++
- Add Python match/case integration test fixtures
- 27 new extractElementTypeFromString unit tests
- 3 for-loop edge cases skipped (declarationTypeNodes scope key lookup)
* fix: address code review findings for Phase 6
- Add missing patternBindingNodeTypes to C# typeConfig (perf gate)
- Add 2048-char input length guard to extractElementTypeFromString
- Skip Python match/case integration tests (call extraction needs query updates)
* reorganise
* fix: Phase 1 bug fixes — Go range semantics, typed_parameter, bracket depth
- Go single-var range correctly returns early for slices/maps (index, not element)
- Go single-var range on channels correctly resolves element type
- Added map_type and channel_type to extractGoElementTypeFromTypeNode
- Added isChannelType helper for channel detection before skip decision
- Added 'typed_parameter' to TYPED_PARAMETER_TYPES for Python annotated params
- Fixed bracket depth tracking in extractElementTypeFromString — only match
selected closeChar at depth 0, return undefined for mismatched brackets
- Un-skipped 3 prematurely skipped tests (TS local const, Python List/Sequence)
- Added tests for map range, single-var range semantics, bracket edge cases
* refactor: Phase 2 architecture — shared helper, required params, decoupled type nodes
- Extract resolveIterableElementType shared helper in shared.ts implementing
3-strategy fallback (declarationTypeNodes → scopeEnv string → AST walk)
- Refactor TS, Python, Go extractors to use shared helper (eliminates 3x duplication)
- Make ForLoopExtractor params required (aligned with PatternBindingExtractor)
- Update Java, Kotlin, C# extractor signatures to accept required params
- Decouple declarationTypeNodes from scopeEnv — capture raw type annotation
nodes BEFORE extractDeclaration for container types (User[], []User, List[User])
- Hybrid approach: direct name extraction + keysBefore fallback for multi-declarator
- Document declarationTypeNodes invariant change (superset of scopeEnv)
* feat: Phase 3 partial — Rust for-loop + C# var foreach Tier 1c
- Rust: add extractForLoopBinding with for_expression support
- Handles &users, &mut users via reference_expression unwrapping
- extractRustElementTypeFromTypeNode: generic_type, reference_type, slice/array
- findRustParamElementType: AST walk with reference/mut pattern unwrapping
- 4 unit tests (Vec<User>, &[User], range expr negative, no-annotation negative)
- C#: upgrade foreach to handle var (implicit_type) via Tier 1c
- extractCSharpElementTypeFromTypeNode: generic_name, array_type, nullable_type
- findCSharpParamElementType: AST walk to method_declaration parameters
- 3 unit tests (var foreach, explicit type regression, no-annotation negative)
* feat: Phase 3 complete — all language gaps + pattern matching
Kotlin Tier 1c:
- Unannotated for-loop resolves via shared helper
- extractKotlinElementTypeFromTypeNode handles type_projection unwrapping
- findKotlinParamElementType walks to function_declaration
Java Tier 1c:
- var foreach resolves via shared helper
- extractJavaElementTypeFromTypeNode handles generic_type, array_type
- findJavaParamElementType walks to method_declaration
TypeScript:
- readonly User[] unwrapped via readonly_type → array_type recursion
C# switch patterns:
- declaration_pattern added to patternBindingNodeTypes
- extractPatternBinding handles standalone declaration_pattern (switch case/expr)
Rust match arms:
- match_arm added to patternBindingNodeTypes
- extractPatternBinding extended with match_arm → match_expression parent traversal
Python:
- as_pattern tries childForFieldName('alias') before positional fallback
Tests: 237 pass (was 224), 13 new tests added
* feat: Phase 4 — known limitation tests, match arm fix, final verification
- Fix Rust match_arm pattern extraction: unwrap match_pattern to get
tuple_struct_pattern inside (tree-sitter-rust wraps in match_pattern node)
- Add first-writer-wins regression test for match arm scope leakage
- Add 5 documented skip tests for known limitations:
- TS destructured for-of (tuple destructuring)
- Python tuple unpacking in for-loops
- TS instanceof narrowing (block-level scoping)
- Rust for with .iter() (method call iterable)
- Ruby block parameters (closure param inference)
Final: 238 passed, 5 skipped (documented limitations), tsc clean
* test: integration tests for all Phase 6 language gaps + fix Rust param pattern field
Integration test fixtures and tests (30 new tests, all with exact match + negative):
Rust for-loop (5 tests):
- for user in &users with Vec<User> → User#save, negative Repo#save
- for repo in &repos with Vec<Repo> → Repo#save, negative User#save
Rust match arm (5 tests):
- match opt { Some(user) => user.save() } → User#save, negative Repo#save
- if let Ok(repo) = res → Repo#save, negative User#save
C# var foreach (5 tests):
- foreach (var user in users) with List<User> → User#Save, negative Repo#Save
- foreach (var repo in repos) with List<Repo> → Repo#Save
C# switch pattern (4 tests):
- is User user → User#Save, case Repo repo → Repo#Save
Kotlin unannotated for (4 tests):
- for (user in users) with List<User> → user.save, negative repo.save
Go map range (3 tests):
- for _, user := range userMap with map[string]User → User#Save, negative
TypeScript readonly (4 tests):
- for (const user of users) with readonly User[] → user.save, negative
Bug fix: type-env.ts parameter branch now falls back to childForFieldName('pattern')
for Rust parameters (Rust uses 'pattern' not 'name' for parameter names)
* test: add assertion bodies to known limitation skip tests
Convert empty skip test stubs to proper tests with parse/buildTypeEnv/expect
assertions following the codebase convention (e.g., call-processor.test.ts:319).
Each skip test now documents the exact expected behavior, so removing .skip
will cause a meaningful failure when the limitation is eventually fixed.
Also clarify Python integration skip tests as call-extraction issues (not
type-env) and Swift integration skips as build-dep issues (self/super
resolution code already exists in type-env.ts).
* feat: resolve 4 known limitation skip tests + method-aware type arg selection
Unskip 4 of 5 type-env known limitations with full integration test coverage:
1. TS destructured for-of: handle array_pattern by binding last named child
to element type. Fix Map<K,V> to return last generic arg (value type).
2. Python dict.items() loop: handle `call` iterables + `pattern_list` left
side. Fix dict[K,V] extraction via type_parameter with last-arg heuristic.
Unwrap `type` wrapper in extractPyElementTypeFromAnnotation.
3. TS instanceof narrowing: add extractPatternBinding for binary_expression
with positional child access. First-writer-wins (not block-scoped).
4. Rust .iter() for-loops: handle call_expression in for_expression value
node by extracting receiver from field_expression.
Method-aware type arg resolution:
- Add TypeArgPosition ('first'|'last') to resolveIterableElementType
- .keys()/.keySet()/.Keys → first type arg (key); all else → last (value)
- Thread position through all 3 strategy callbacks in TS/Rust/Python
- Add predefined_type to extractSimpleTypeName for TS primitives (string etc)
New fixtures: rust-iter-for-loop, typescript-destructured-for-of,
typescript-instanceof-narrowing, python-dict-items-loop.
248 unit tests pass (6 new), 1 skip (Ruby block params).
* feat: container descriptor table for generic type arg resolution
Replace simple KEY_METHODS heuristic with CONTAINER_DESCRIPTORS table
that maps 30+ container types across all languages to their type parameter
semantics per access method.
Key improvements:
- Container-aware resolution: HashMap.iter() correctly yields V (arity 2),
while Vec.iter() yields T (arity 1) — same method, different semantics
- Cross-language coverage: Map/HashMap/BTreeMap/dict/Dict/Dictionary/
ConcurrentHashMap + List/Vec/Set/HashSet/Queue/Deque/Stack etc.
- Method categorization: keyMethods (keys/keySet/Keys) vs valueMethods
(values/get/pop/iter/first/last) per container type
- Fallback for unknown containers: still uses method name heuristic,
so MyCache<K,V>.keys() correctly returns first arg
- Exported getContainerDescriptor() for future heritage-chain lookups
Each language extractor now passes containerTypeName from scopeEnv to
methodToTypeArgPosition for descriptor-aware resolution.
252 unit tests pass (4 new descriptor tests), 1 skip (Ruby).
* feat: method-aware for-loop extractors + integration tests for all languages
Upgrade 4 existing extractors + create 3 new ones for full cross-language
coverage of call_expression iterables and container descriptor resolution:
Upgraded (add call expr iterable + methodToTypeArgPosition):
- Java: method_invocation (data.keySet(), data.values())
- Kotlin: navigation_expression + call_expression (data.keys, data.values())
- C#: member_access_expression + invocation_expression (data.Keys, data.Values)
- Go: TypeArgPosition threading for Go 1.18+ generics
New for-loop extractors:
- C++: for_range_loop with auto& unwrapping, template_type + qualified_identifier
(std::vector<User>) extraction, explicit vs auto type handling
- PHP: foreach_statement with simple/key-value/by-reference forms, PHPDoc
@param priority over AST array type
- Ruby: for-in with YARD @param type resolution via comment parsing
Integration test fixtures + tests for all 6 languages:
- java-map-keys-values (Map.values() + List iteration)
- kotlin-map-keys-values (HashMap.values + List iteration)
- csharp-dictionary-keys-values (Dictionary.Values foreach)
- cpp-range-for (auto& + const auto& range-based for)
- php-foreach-loop (foreach with PHPDoc @param User[])
- ruby-for-in-loop (for-in with YARD @param Array<User>)
Bugs fixed during integration testing:
- C++: qualified_identifier (std::vector) not unwrapped to template_type
- PHP: extractParameter overwrote PHPDoc-derived types with bare 'array'
252 unit tests pass, 201 integration tests pass across 6 languages.
* fix: update extractElementTypeFromString tests for last-arg default
TypeArgPosition change (default 'last') broke 5 existing tests expecting
first arg from multi-arg generics. Updated expectations and added explicit
pos='first' tests for key type extraction.
* fix: rename C++ fixture files to correct case for case-sensitive CI
On case-sensitive filesystems (Linux/macOS CI), git tracked both the old
lowercase files (app.cpp, user.h) and the new uppercase files (App.cpp,
User.h) as separate files. The pipeline processed both, causing the old
app.cpp (with explicit User& type) to interfere with the new auto& test.
Removes old lowercase entries and re-adds with uppercase casing to match
the #include directives in the fixture.
* feat: PR #318 review findings — pattern bindings, member access iterables, structured bindings
Address all 7 genuine gaps identified in PR #318 deep code review:
- Kotlin: add extractKotlinPatternBinding for when/is (type_test AST node)
with allowPatternBindingOverwrite for smart-cast semantics
- Java: add type_pattern branch for Java 17+ switch pattern variables
- TypeScript: explicit object_pattern skip in for-of (no false bindings)
- Cross-language: member access iterables (self.users, this.users, repo.users)
across all 10 language extractors
- C++: structured_binding_declarator handling in range-for (last-child heuristic)
- Rust: closure_parameter added to TYPED_PARAMETER_TYPES
- PHP: normalizePhpType handles angle-bracket generics (Collection<User>)
Code review fixes applied:
- Remove 4 debug console.log statements (c-cpp.ts, call-processor.ts)
- Hoist KNOWN_CONTAINER_PROPS to module scope (csharp.ts)
- Guard keysBefore allocation behind typeNode check (type-env.ts)
- Add depth limits (50) to 7 recursive type extraction functions
- Add 2048-char length cap to extractSimpleTypeName
- Fix PHP/Ruby missing typeArgPos parameter in resolveIterableElementType
Integration test fixtures: kotlin-when-pattern, java-switch-pattern,
cpp-structured-binding, typescript-member-access-for-loop,
python-member-access-for-loop
* fix: position-indexed when/is bindings, Kotlin param extraction, HashMap.values for-loop
Three root causes for failing Kotlin integration tests:
1. When/is multi-arm resolution: flat scopeEnv stored only the last arm's
type (last-writer-wins). Added PatternOverrides with AST range indexing
so each when arm resolves to its narrowed type independently.
2. HashMap.values for-loop: navigation_expression without call_suffix was
classified as bare property access (iterableName='values' instead of
'data'). Now tries object-as-iterable + property-as-method first, with
fallback to property-as-iterable for this.users patterns.
3. Kotlin parameter extraction: tree-sitter-kotlin parameter nodes use
positional children (simple_identifier, user_type) not named fields
(name, type). Added fallback to findChildByType in both
extractKotlinParameter and extractTypeBinding.
Integration tests added for .keys/.values/Set/MutableMap iteration,
3-arm when/is, multi-call within arms, and when+else branch.
* feat: enhance PHP type resolution for generics and member access in foreach loops
* feat: Phase 6.1 type resolution gap closure — container descriptors, recursive_pattern, class fields
Add 13 missing container type descriptors (Collection, MutableMap, Stream, SortedSet, etc.)
to CONTAINER_DESCRIPTORS for correct element type extraction across C#, Kotlin, and Java.
Extend C# pattern binding to handle recursive_pattern (obj is User { Name: "Alice" } u)
in both is-expression and switch expression contexts.
Add TypeScript class field declaration support (public_field_definition) so for-loop
iteration over this.fieldName resolves element types from class field type annotations.
Includes file-scope fallback in resolveIterableElementType and nested member_expression
handling for this.field.method() patterns.
* docs: add type resolution system documentation with roadmap
Covers the full architecture, resolution tiers (0-2), scope model,
language feature matrix, container descriptors, pipeline integration,
and the Phase 7-9 roadmap for cross-scope propagation, field-type
resolution, and return-type-aware binding.
* feat: Phase 6.2 review findings — C# nested member foreach, C++ deref range-for, Java field_access
Close two gaps found during fourth-pass review of PR #318:
- C# foreach (var user in this.data.Values): nested member_access_expression
now extracts intermediate property name for scopeEnv lookup
- C++ for (auto& user : *ptr): pointer_expression dereference now recognized
as range-for iterable
Root causes fixed in shared infrastructure:
- extractSimpleTypeName: add template_type (C++) and generic_name (C#)
- extractGenericTypeArgs: add generic_name for consistency
- type-env.ts: unwrap variable_declaration wrapper in field_declaration
for declarationTypeNodes capture (zero-allocation manual loop)
Additional review findings addressed:
- Java: add field_access handler for this.data.values() in method_invocation
- C++ pointer_expression: document limitation (*identifier only)
- TypeScript: fix stale comment about property_identifier
All 525 tests pass (278 unit + 247 integration).
* perf: optimize type resolution pipeline — worker threshold, skip graph phases, AST pruning
- Skip worker pool creation for small repos (<15 files or <512KB) — saves 100-400ms
- Add skipGraphPhases option to runPipelineFromRepo to skip MRO/community/process phases
- Add conservative SKIP_SUBTREE_TYPES for leaf-only AST nodes (string, comment, number)
- Pre-compute interestingNodeTypes set — single Set.has() replaces 3 checks per node
- Add fastStripNullable — skip full stripNullable for simple identifiers (90%+ case)
- Replace .children?.find() with manual for loops in extractFunctionName (no array alloc)
- Add hookTimeout: 120000 to vitest.config.ts for CI beforeAll hooks
* fix: review findings — remove template_string from SKIP_SUBTREE_TYPES, handle bare nullable keywords
- Remove template_string and concatenated_string from SKIP_SUBTREE_TYPES
(template literals contain interpolated expressions with typed code)
- Add FAST_NULLABLE_KEYWORDS check to fastStripNullable for behavioral
parity with stripNullable on bare null/undefined/void/None/nil
- Add explanatory comment on extractPendingAssignment scopeEnv guard
* feat: add type resolution system and roadmap documentation
1195 lines
44 KiB
TypeScript
1195 lines
44 KiB
TypeScript
/**
|
|
* C#: heritage resolution via base_list + ambiguous namespace-import refusal
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import path from 'path';
|
|
import {
|
|
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
|
runPipelineFromRepo, type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Heritage: class + interface resolution via base_list
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# heritage resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-proj'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects exactly 3 classes and 2 interfaces', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['BaseEntity', 'User', 'UserService']);
|
|
expect(getNodesByLabel(result, 'Interface')).toEqual(['ILogger', 'IRepository']);
|
|
});
|
|
|
|
it('emits exactly 1 EXTENDS edge: User → BaseEntity', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
expect(extends_.length).toBe(1);
|
|
expect(extends_[0].source).toBe('User');
|
|
expect(extends_[0].target).toBe('BaseEntity');
|
|
});
|
|
|
|
it('emits exactly 1 IMPLEMENTS edge: User → IRepository', () => {
|
|
const implements_ = getRelationships(result, 'IMPLEMENTS');
|
|
expect(implements_.length).toBe(1);
|
|
expect(implements_[0].source).toBe('User');
|
|
expect(implements_[0].target).toBe('IRepository');
|
|
});
|
|
|
|
it('emits CALLS edges from CreateUser (constructor + member calls)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
expect(calls.length).toBe(4);
|
|
const targets = edgeSet(calls);
|
|
expect(targets).toContain('CreateUser → User'); // new User() constructor
|
|
expect(targets).toContain('CreateUser → Validate'); // user.Validate() — receiver-typed
|
|
expect(targets).toContain('CreateUser → Save'); // _repo.Save() — receiver-typed
|
|
expect(targets).toContain('CreateUser → Log'); // _logger.Log() — receiver-typed
|
|
});
|
|
|
|
it('resolves all CALLS from CreateUser via import-resolved or unique-global', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
// C# non-aliased `using Namespace;` imports don't populate NamedImportMap
|
|
// (namespace-scoped imports can't bind to individual symbols).
|
|
// Calls resolve via directory-based PackageMap (import-resolved) when ambiguous,
|
|
// or via unique-global when the symbol name is globally unique.
|
|
for (const call of calls) {
|
|
expect(['import-resolved', 'global']).toContain(call.rel.reason);
|
|
}
|
|
});
|
|
|
|
it('resolves new User() to the User class via constructor discrimination', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const ctorCall = calls.find(c => c.target === 'User');
|
|
expect(ctorCall).toBeDefined();
|
|
expect(ctorCall!.targetLabel).toBe('Class');
|
|
});
|
|
|
|
it('detects 4 namespaces', () => {
|
|
const ns = getNodesByLabel(result, 'Namespace');
|
|
expect(ns.length).toBe(4);
|
|
});
|
|
|
|
it('detects properties on classes', () => {
|
|
const props = getNodesByLabel(result, 'Property');
|
|
expect(props).toContain('Id');
|
|
expect(props).toContain('Name');
|
|
});
|
|
|
|
it('no OVERRIDES edges target Property nodes', () => {
|
|
const overrides = getRelationships(result, 'OVERRIDES');
|
|
for (const edge of overrides) {
|
|
const target = result.graph.getNode(edge.rel.targetId);
|
|
expect(target).toBeDefined();
|
|
expect(target!.label).not.toBe('Property');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ambiguous: using-namespace can't disambiguate same-named types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# ambiguous symbol resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-ambiguous'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects 2 Handler classes and 2 IProcessor interfaces', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes.filter(n => n === 'Handler').length).toBe(2);
|
|
const ifaces = getNodesByLabel(result, 'Interface');
|
|
expect(ifaces.filter(n => n === 'IProcessor').length).toBe(2);
|
|
});
|
|
|
|
it('heritage targets are synthetic (correct refusal for ambiguous namespace import)', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
const implements_ = getRelationships(result, 'IMPLEMENTS');
|
|
|
|
expect(extends_.length).toBe(1);
|
|
expect(extends_[0].source).toBe('UserHandler');
|
|
expect(implements_.length).toBe(1);
|
|
expect(implements_[0].source).toBe('UserHandler');
|
|
|
|
// The key invariant: no edge points to Other/
|
|
if (extends_[0].targetFilePath) {
|
|
expect(extends_[0].targetFilePath).not.toMatch(/Other\//);
|
|
}
|
|
if (implements_[0].targetFilePath) {
|
|
expect(implements_[0].targetFilePath).not.toMatch(/Other\//);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('C# call resolution with arity filtering', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-calls'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('resolves CreateUser → WriteAudit to Utils/OneArg.cs via arity narrowing', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
expect(calls.length).toBe(1);
|
|
expect(calls[0].source).toBe('CreateUser');
|
|
expect(calls[0].target).toBe('WriteAudit');
|
|
expect(calls[0].targetFilePath).toBe('Utils/OneArg.cs');
|
|
expect(calls[0].rel.reason).toBe('import-resolved');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Member-call resolution: obj.Method() resolves through pipeline
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# member-call resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-member-calls'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('resolves ProcessUser → Save as a member call on User', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(c => c.target === 'Save');
|
|
expect(saveCall).toBeDefined();
|
|
expect(saveCall!.source).toBe('ProcessUser');
|
|
expect(saveCall!.targetFilePath).toBe('Models/User.cs');
|
|
});
|
|
|
|
it('detects User class and Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Method')).toContain('Save');
|
|
});
|
|
|
|
it('emits HAS_METHOD edge from User to Save', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const edge = hasMethod.find(e => e.source === 'User' && e.target === 'Save');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Primary constructor resolution: class User(string name, int age) { }
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# primary constructor resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-primary-ctors'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects Constructor nodes for primary constructors on class and record', () => {
|
|
const ctors = getNodesByLabel(result, 'Constructor');
|
|
expect(ctors).toContain('User');
|
|
expect(ctors).toContain('Person');
|
|
});
|
|
|
|
it('primary constructor has correct parameter count', () => {
|
|
let userCtorParams: number | undefined;
|
|
let personCtorParams: number | undefined;
|
|
result.graph.forEachNode(n => {
|
|
if (n.label === 'Constructor' && n.properties.name === 'User') {
|
|
userCtorParams = n.properties.parameterCount as number;
|
|
}
|
|
if (n.label === 'Constructor' && n.properties.name === 'Person') {
|
|
personCtorParams = n.properties.parameterCount as number;
|
|
}
|
|
});
|
|
expect(userCtorParams).toBe(2);
|
|
expect(personCtorParams).toBe(2);
|
|
});
|
|
|
|
it('resolves new User(...) as a CALLS edge to the Constructor node', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const ctorCall = calls.find(c => c.target === 'User');
|
|
expect(ctorCall).toBeDefined();
|
|
expect(ctorCall!.source).toBe('Run');
|
|
expect(ctorCall!.targetLabel).toBe('Constructor');
|
|
expect(ctorCall!.targetFilePath).toBe('Models/User.cs');
|
|
});
|
|
|
|
it('also resolves user.Save() as a method call', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(c => c.target === 'Save');
|
|
expect(saveCall).toBeDefined();
|
|
expect(saveCall!.source).toBe('Run');
|
|
});
|
|
|
|
it('emits HAS_METHOD edge from User class to User constructor', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const edge = hasMethod.find(e => e.source === 'User' && e.target === 'User');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
|
|
it('emits HAS_METHOD edge from Person record to Person constructor', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const edge = hasMethod.find(e => e.source === 'Person' && e.target === 'Person');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Receiver-constrained resolution: typed variables disambiguate same-named methods
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# receiver-constrained resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-receiver-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, both with Save methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves user.Save() to User.Save and repo.Save() to Repo.Save via receiver typing', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter(c => c.target === 'Save');
|
|
expect(saveCalls.length).toBe(2);
|
|
|
|
const userSave = saveCalls.find(c => c.targetFilePath === 'Models/User.cs');
|
|
const repoSave = saveCalls.find(c => c.targetFilePath === 'Models/Repo.cs');
|
|
|
|
expect(userSave).toBeDefined();
|
|
expect(repoSave).toBeDefined();
|
|
expect(userSave!.source).toBe('ProcessEntities');
|
|
expect(repoSave!.source).toBe('ProcessEntities');
|
|
});
|
|
|
|
it('resolves constructor calls for both User and Repo', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userCtor = calls.find(c => c.target === 'User');
|
|
const repoCtor = calls.find(c => c.target === 'Repo');
|
|
expect(userCtor).toBeDefined();
|
|
expect(repoCtor).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Alias import resolution: using U = Models.User resolves U → User
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# alias import resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-alias-imports'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects Main, Repo, and User classes', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['Main', 'Repo', 'User']);
|
|
});
|
|
|
|
it('resolves u.Save() to User.cs and r.Persist() to Repo.cs via alias', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(c => c.target === 'Save');
|
|
const persistCall = calls.find(c => c.target === 'Persist');
|
|
|
|
expect(saveCall).toBeDefined();
|
|
expect(saveCall!.source).toBe('Run');
|
|
expect(saveCall!.targetLabel).toBe('Method');
|
|
expect(saveCall!.targetFilePath).toBe('Models/User.cs');
|
|
|
|
expect(persistCall).toBeDefined();
|
|
expect(persistCall!.source).toBe('Run');
|
|
expect(persistCall!.targetLabel).toBe('Method');
|
|
expect(persistCall!.targetFilePath).toBe('Models/Repo.cs');
|
|
});
|
|
|
|
it('emits exactly 2 IMPORTS edges via alias resolution', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
expect(imports.length).toBe(2);
|
|
expect(edgeSet(imports)).toEqual([
|
|
'Main.cs → Repo.cs',
|
|
'Main.cs → User.cs',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Variadic resolution: params string[] doesn't get filtered by arity
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# variadic call resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-variadic-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('resolves call to params method Record(params string[]) in Logger.cs', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const logCall = calls.find(c => c.target === 'Record');
|
|
expect(logCall).toBeDefined();
|
|
expect(logCall!.source).toBe('Execute');
|
|
expect(logCall!.targetFilePath).toBe('Utils/Logger.cs');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Local shadow: same-file definition takes priority over imported name
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# local definition shadows import', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-local-shadow'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('resolves Run → Save to same-file definition, not the imported one', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(c => c.target === 'Save' && c.source === 'Run');
|
|
expect(saveCall).toBeDefined();
|
|
expect(saveCall!.targetFilePath).toBe('App/Main.cs');
|
|
});
|
|
|
|
it('does NOT resolve Save to Logger.cs', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveToUtils = calls.find(c => c.target === 'Save' && c.targetFilePath === 'Utils/Logger.cs');
|
|
expect(saveToUtils).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// For-each loop element typing: foreach (User user in users) user.Save()
|
|
// C#: explicit type in foreach_statement binds loop variable
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# foreach loop element type resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-foreach'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, both with Save methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves user.Save() in foreach to User#Save (not Repo#Save)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c => c.target === 'Save' && c.targetFilePath === 'Models/User.cs');
|
|
expect(userSave).toBeDefined();
|
|
expect(userSave!.source).toBe('ProcessEntities');
|
|
});
|
|
|
|
it('resolves repo.Save() in foreach to Repo#Save (not User#Save)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c => c.target === 'Save' && c.targetFilePath === 'Models/Repo.cs');
|
|
expect(repoSave).toBeDefined();
|
|
expect(repoSave!.source).toBe('ProcessEntities');
|
|
});
|
|
|
|
it('emits exactly 2 Save() CALLS edges (one per receiver type)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter(c => c.target === 'Save');
|
|
expect(saveCalls.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// this.Save() resolves to enclosing class's own Save method
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# this resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-self-this-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, each with a Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['Repo', 'User']);
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves this.Save() inside User.Process to User.Save, not Repo.Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(c => c.target === 'Save' && c.source === 'Process');
|
|
expect(saveCall).toBeDefined();
|
|
expect(saveCall!.targetFilePath).toBe('src/Models/User.cs');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Parent class resolution: EXTENDS + IMPLEMENTS via base_list
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# parent resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-parent-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects BaseModel and User classes plus ISerializable interface', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['BaseModel', 'User']);
|
|
expect(getNodesByLabel(result, 'Interface')).toEqual(['ISerializable']);
|
|
});
|
|
|
|
it('emits EXTENDS edge: User → BaseModel (from base_list)', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
expect(extends_.length).toBe(1);
|
|
expect(extends_[0].source).toBe('User');
|
|
expect(extends_[0].target).toBe('BaseModel');
|
|
});
|
|
|
|
it('emits IMPLEMENTS edge: User → ISerializable', () => {
|
|
const implements_ = getRelationships(result, 'IMPLEMENTS');
|
|
expect(implements_.length).toBe(1);
|
|
expect(implements_[0].source).toBe('User');
|
|
expect(implements_[0].target).toBe('ISerializable');
|
|
});
|
|
|
|
it('all heritage edges point to real graph nodes', () => {
|
|
for (const edge of [...getRelationships(result, 'EXTENDS'), ...getRelationships(result, 'IMPLEMENTS')]) {
|
|
const target = result.graph.getNode(edge.rel.targetId);
|
|
expect(target).toBeDefined();
|
|
expect(target!.properties.name).toBe(edge.target);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// base.Save() resolves to parent class's Save method
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# base resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-super-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects BaseModel, User, and Repo classes', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['BaseModel', 'Repo', 'User']);
|
|
});
|
|
|
|
it('resolves base.Save() inside User to BaseModel.Save, not Repo.Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const baseSave = calls.find(c => c.source === 'Save' && c.target === 'Save'
|
|
&& c.targetFilePath === 'src/Models/BaseModel.cs');
|
|
expect(baseSave).toBeDefined();
|
|
const repoSave = calls.find(c => c.target === 'Save' && c.targetFilePath === 'src/Models/Repo.cs');
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// base.Save() resolves to generic parent class's Save method
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# generic parent base resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-generic-parent-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects BaseModel, User, and Repo classes', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['BaseModel', 'Repo', 'User']);
|
|
});
|
|
|
|
it('resolves base.Save() inside User to BaseModel.Save, not Repo.Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const baseSave = calls.find(c => c.source === 'Save' && c.target === 'Save'
|
|
&& c.targetFilePath === 'src/Models/BaseModel.cs');
|
|
expect(baseSave).toBeDefined();
|
|
const repoSave = calls.find(c => c.target === 'Save' && c.targetFilePath === 'src/Models/Repo.cs');
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pattern matching: `if (animal is Dog dog)` binds `dog` as type `Dog`
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# is pattern matching resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-pattern-matching'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects Animal, Dog, and Cat classes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('Animal');
|
|
expect(classes).toContain('Dog');
|
|
expect(classes).toContain('Cat');
|
|
});
|
|
|
|
it('detects Bark and Meow methods', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('Bark');
|
|
expect(methods).toContain('Meow');
|
|
});
|
|
|
|
it('resolves dog.Bark() to Dog.Bark via is-pattern type binding', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const barkCall = calls.find(c => c.target === 'Bark');
|
|
expect(barkCall).toBeDefined();
|
|
expect(barkCall!.source).toBe('HandleAnimal');
|
|
expect(barkCall!.targetFilePath).toBe('Models/Animal.cs');
|
|
});
|
|
|
|
it('emits EXTENDS edges for Dog and Cat', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
const dogExtends = extends_.find(e => e.source === 'Dog');
|
|
const catExtends = extends_.find(e => e.source === 'Cat');
|
|
expect(dogExtends).toBeDefined();
|
|
expect(dogExtends!.target).toBe('Animal');
|
|
expect(catExtends).toBeDefined();
|
|
expect(catExtends!.target).toBe('Animal');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Return type inference: var user = svc.GetUser("alice"); user.Save()
|
|
// C#'s CONSTRUCTOR_BINDING_SCANNER handles `var` declarations with
|
|
// invocation_expression values, enabling end-to-end return type inference.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# return type inference via var + invocation', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-return-type'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User, UserService, and Repo classes', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('UserService');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
});
|
|
|
|
it('detects Save on both User and Repo, plus GetUser', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('Save');
|
|
expect(methods).toContain('GetUser');
|
|
// Repo.Save is also detected, proving the disambiguation test is meaningful
|
|
expect(methods.filter((m: string) => m === 'Save').length).toBe(2);
|
|
});
|
|
|
|
it('resolves user.Save() to User#Save (not Repo#Save) via return type of GetUser(): User', () => {
|
|
// scanConstructorBinding binds `var user = svc.GetUser()` → calleeName "GetUser".
|
|
// processCallsFromExtracted verifies GetUser's returnType is "User" via
|
|
// PackageMap resolution of `using ReturnType.Models;`, then receiver filtering
|
|
// resolves user.Save() to User#Save (not Repo#Save).
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'Run' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
// Must NOT resolve to Repo.Save — that would mean disambiguation failed
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'Run' && c.targetFilePath.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('C# null-conditional call resolution (user?.Save())', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-null-conditional'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes with competing Save methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter((m: string) => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('captures null-conditional user?.Save() call', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter(c => c.target === 'Save' && c.source === 'Process');
|
|
expect(saveCalls.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('resolves user?.Save() to User#Save via receiver typing', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'Process' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves repo?.Save() to Repo#Save via receiver typing', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'Process' && c.targetFilePath.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT cross-contaminate (exactly 1 Save per receiver file)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter(c => c.target === 'Save' && c.source === 'Process');
|
|
const userTargeted = saveCalls.filter(c => c.targetFilePath.includes('User.cs'));
|
|
const repoTargeted = saveCalls.filter(c => c.targetFilePath.includes('Repo.cs'));
|
|
expect(userTargeted.length).toBe(1);
|
|
expect(repoTargeted.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# async/await constructor binding resolution
|
|
// Verifies that `var user = await svc.GetUserAsync()` correctly unwraps the
|
|
// await_expression to find the invocation_expression underneath, producing a
|
|
// constructor binding that enables receiver-based disambiguation of user.Save().
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# async await constructor binding resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-async-binding'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User, UserService, and OrderService classes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('User');
|
|
expect(classes).toContain('UserService');
|
|
expect(classes).toContain('OrderService');
|
|
});
|
|
|
|
it('detects competing Save methods on User and Order', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('Save');
|
|
expect(methods).toContain('GetUserAsync');
|
|
expect(methods).toContain('GetOrderAsync');
|
|
});
|
|
|
|
it('resolves user.Save() after await to User#Save via return type inference', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessUser' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('user.Save() does NOT resolve to Order#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrongSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessUser' && c.targetFilePath.includes('Order.cs'),
|
|
);
|
|
expect(wrongSave).toBeUndefined();
|
|
});
|
|
|
|
it('resolves order.Save() after await to Order#Save via return type inference', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const orderSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessOrder' && c.targetFilePath.includes('Order.cs'),
|
|
);
|
|
expect(orderSave).toBeDefined();
|
|
});
|
|
|
|
it('order.Save() does NOT resolve to User#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrongSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessOrder' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
expect(wrongSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Assignment chain propagation (Phase 4.3)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# assignment chain propagation', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-assignment-chain'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes each with a Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves alias.Save() to User#Save via assignment chain', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
// Positive: alias.Save() must resolve to User#Save
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessEntities' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('alias.Save() does NOT resolve to Repo#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
// Negative: alias comes from User, so only one edge to User.cs
|
|
const wrongCall = calls.filter(c =>
|
|
c.target === 'Save' && c.source === 'ProcessEntities' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
expect(wrongCall.length).toBe(1);
|
|
});
|
|
|
|
it('resolves rAlias.Save() to Repo#Save via assignment chain', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
// Positive: rAlias.Save() must resolve to Repo#Save
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessEntities' && c.targetFilePath.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
|
|
it('each alias resolves to its own class, not the other', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessEntities' && c.targetFilePath.includes('User.cs'),
|
|
);
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessEntities' && c.targetFilePath.includes('Repo.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
expect(repoSave).toBeDefined();
|
|
expect(userSave!.targetFilePath).not.toBe(repoSave!.targetFilePath);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# mixed declarations: assignment chain + is-pattern in the same file.
|
|
// Tests that the type guard in extractPendingAssignment correctly skips
|
|
// is_pattern_expression nodes while still handling local_declaration_statement.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# assignment chain + is-pattern coexistence', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-mixed-decl-chain'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes each with a Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves alias.Save() to User#Save via assignment chain', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessWithChain' && c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('assignment chain alias does NOT resolve to Repo#Save (negative)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrongCall = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessWithChain' && c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(wrongCall).toBeUndefined();
|
|
});
|
|
|
|
it('resolves u.Save() to User#Save via is-pattern binding', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const patternSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessWithPattern' && c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
expect(patternSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves alias.Save() to Repo#Save via Repo assignment chain', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessRepoChain' && c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
|
|
it('Repo chain alias does NOT resolve to User#Save (negative)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrongCall = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessRepoChain' && c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
expect(wrongCall).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# is-pattern disambiguation: `if (obj is User user)` should bind user → User
|
|
// and resolve user.Save() to User#Save, NOT Repo#Save.
|
|
// Validates the Phase 5.2 is_pattern_expression extraction in extractDeclaration.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# is-pattern type binding disambiguation (Phase 5.2)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-is-pattern'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes each with a Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'Save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves user.Save() inside if (obj is User user) to User#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' &&
|
|
c.source === 'Process' &&
|
|
c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT resolve user.Save() to Repo#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' &&
|
|
c.source === 'Process' &&
|
|
c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Chained method calls: svc.GetUser().Save()
|
|
// Tests that C# chain call resolution correctly infers the intermediate
|
|
// receiver type from GetUser()'s return type and resolves Save() to User.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# chained method call resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-chain-call'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User, Repo, and UserService classes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('User');
|
|
expect(classes).toContain('Repo');
|
|
expect(classes).toContain('UserService');
|
|
});
|
|
|
|
it('detects GetUser and Save methods', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('GetUser');
|
|
expect(methods).toContain('Save');
|
|
});
|
|
|
|
it('resolves svc.GetUser().Save() to User#Save via chain resolution', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' &&
|
|
c.source === 'ProcessUser' &&
|
|
c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT resolve svc.GetUser().Save() to Repo#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' &&
|
|
c.source === 'ProcessUser' &&
|
|
c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# var foreach Tier 1c: foreach (var user in users) with List<User> param
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# var foreach type resolution (Tier 1c)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-var-foreach'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, both with Save methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
});
|
|
|
|
it('detects methods on both classes', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods.filter(m => m === 'Save').length).toBe(2);
|
|
expect(methods).toContain('ProcessUsers');
|
|
expect(methods).toContain('ProcessRepos');
|
|
});
|
|
|
|
it('resolves direct calls with explicit parameter types (u.Save, r.Save)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const directUserSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'Direct' && c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
const directRepoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'Direct' && c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(directUserSave).toBeDefined();
|
|
expect(directRepoSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves user.Save() in var foreach to User#Save via Tier 1c', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessUsers' && c.targetFilePath?.includes('User.cs'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves repo.Save() in var foreach to Repo#Save via Tier 1c', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessRepos' && c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT cross-resolve user.Save() to Repo#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrong = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessUsers' && c.targetFilePath?.includes('Repo.cs'),
|
|
);
|
|
expect(wrong).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# switch pattern: switch (obj) { case User user: user.Save(); }
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# switch pattern type resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-switch-pattern'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
});
|
|
|
|
it('resolves user.Save() via is-pattern to User#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c => c.target === 'Save' && c.targetFilePath === 'Models/User.cs');
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves repo.Save() via switch case pattern to Repo#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c => c.target === 'Save' && c.targetFilePath === 'Models/Repo.cs');
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# Dictionary .Values foreach — member_access_expression resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# Dictionary .Values foreach resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-dictionary-keys-values'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User class with Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
});
|
|
|
|
it('resolves user.Save() via Dictionary.Values to User#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('User'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT resolve user.Save() to Repo#Save (negative)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrongSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('Repo'),
|
|
);
|
|
expect(wrongSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# recursive_pattern: obj is User { Name: "Alice" } u — Phase 6.1
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# recursive_pattern type resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-recursive-pattern'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes with Save methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
|
});
|
|
|
|
it('resolves u.Save() via recursive_pattern is-expression to User#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.targetFilePath?.includes('User'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves r.Save() via recursive_pattern switch expression to Repo#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(c =>
|
|
c.target === 'Save' && c.targetFilePath?.includes('Repo'),
|
|
);
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves exactly one Save call per target class (no cross-resolution)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter(c => c.target === 'Save' && c.source === 'ProcessWithRecursivePattern');
|
|
const toUser = saveCalls.filter(c => c.targetFilePath?.includes('User'));
|
|
const toRepo = saveCalls.filter(c => c.targetFilePath?.includes('Repo'));
|
|
// u.Save() → User#Save only, r.Save() → Repo#Save only
|
|
expect(toUser.length).toBe(1);
|
|
expect(toRepo.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// C# nested member access with container property: this.data.Values
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('C# nested member access foreach (this.data.Values)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'csharp-nested-member-foreach'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User class with Save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
});
|
|
|
|
it('resolves user.Save() via this.data.Values to User#Save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('User'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT resolve user.Save() to Repo#Save (negative)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const wrongSave = calls.find(c =>
|
|
c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('Repo'),
|
|
);
|
|
expect(wrongSave).toBeUndefined();
|
|
});
|
|
});
|