GitNexus/gitnexus/test/integration/resolvers/csharp-1924.test.ts
Abhinav Pandey 89b02286ad
fix(csharp): qualified/alias constructor names, : base/: this initializers, generic type-arg strip (#2046)
* fix(csharp): bind qualified constructor names, capture : base/: this, fix generic strip

Mirrors the Java #1928 parsing-layer fixes for the C# scope-resolution path —
the same three defect classes exist verbatim in C#:

- Qualified / qualified-generic / alias-qualified constructor calls
  (`new Ns.Foo()`, `new A.B.Foo()`, `new Ns.Box<int>()`, `new MyAlias::Foo()`,
  `new global::Foo()`) bound only `@reference.call.constructor.qualified` with no
  `@reference.name`, so the central extractor fell back to the whole-expression
  anchor and the reference name became the raw `new Ns.Foo()` text (never
  resolved). Derive the simple-name tail via the existing `terminalTypeNameNode`
  helper (handles qualified_name, generic tail, and alias_qualified_name), and
  add a query arm for the top-level `alias_qualified_name` shape that was not
  captured at all.

- `: base(...)` / `: this(...)` explicit constructor initializers, modeled by
  tree-sitter as `constructor_initializer` and never matched by the scope query,
  dropped the chained-constructor CALLS edges. Synthesize them: `this` → enclosing
  type name; `base` → the base type's bare name (first base-list entry, which C#
  requires to be the base class). Arity attached for overload disambiguation.

- `interpretCsharpTypeBinding`'s qualifier strip used `lastIndexOf('.')` over the
  whole string, cutting inside a qualified generic type ARGUMENT
  (`Dictionary<string, Ns.User>` → `User>`). Make stripQualifier generic-aware:
  reduce only the segment before the first `<`, re-attaching the generic suffix —
  multi-arg generics stay intact so the `.Values`/`.Keys` collection-accessor
  unwrap keeps working.

Tests: capture-level unit tests for every constructor shape (incl. alias-qualified,
double-match guard) and `: base`/`: this` (incl. struct/record/mixed-base);
interpretCsharpTypeBinding unit tests (the corruption case + nullable/nested/
unknown-generic edges); end-to-end resolver tests with new fixtures. The
csharp-captures golden was regenerated — drift is purely additive (only the new
fixtures; zero existing-fixture digests changed).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(csharp): enhance constructor resolution and namespace qualification

- Implemented qualified constructor name binding to resolve collisions between types in different namespaces.
- Added support for `: base(...)` and `: this(...)` constructor initializers to ensure correct edge emission in the scope resolution.
- Improved generic argument stripping to prevent incorrect parsing of qualified types.
- Introduced tests for new features, including handling of interface-only base classes and qualified constructor calls.

This update addresses issues related to constructor resolution and namespace qualification, ensuring accurate type references in C# code. Tests have been added to validate these changes.

* fix(csharp): implement namespace prefix tagging for file-level type definitions

- Updated the C# ingestion process to tag file-level type definitions with their enclosing namespace path using a new `namespacePrefix` field, without altering the `qualifiedName`.
- Enhanced the scope resolver to utilize the `namespacePrefix` for resolving same-tail collisions in constructor calls, improving accuracy in type resolution.
- Added unit tests to validate the new functionality, ensuring that namespace prefixes are correctly applied to both block-scoped and file-scoped types, while leaving namespace-free types untagged.

This change addresses issues related to namespace qualification and constructor resolution in C# code, facilitating better handling of type references.

* refactor(scope-resolution): share isOverloadableCallable via util

Extract the ctor/function/method overload predicate into
callable-labels.ts so graph-bridge registration and lookup stay aligned
without duplicated private copies in ids.ts and node-lookup.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-05 07:04:57 +01:00

100 lines
3.9 KiB
TypeScript

/**
* C# parsing-layer coverage gaps mirroring the Java #1928 findings — end-to-end.
*
* - F35: qualified / qualified-generic constructor calls (`new Ns.Foo()`,
* `new Ns.Box<int>()`) resolve to the target constructor/class instead
* of dropping the edge on a corrupted `Ns.Foo` reference name.
* - F38: `: base(...)` / `: this(...)` constructor initializers emit CALLS
* edges to the base / sibling constructor.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js';
describe('C# qualified constructor resolution (F35, mirror of Java #1928)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'csharp-qualified-constructor'),
() => {},
);
}, 60000);
it('resolves `new Models.Widget()` to the Widget type', () => {
const calls = getRelationships(result, 'CALLS');
const widget = calls.find((c) => c.source === 'Make' && c.target === 'Widget');
expect(widget).toBeDefined();
expect(['Class', 'Constructor']).toContain(widget!.targetLabel);
});
it('resolves `new Models.Box<int>()` to the Box type', () => {
const calls = getRelationships(result, 'CALLS');
const box = calls.find((c) => c.source === 'Make' && c.target === 'Box');
expect(box).toBeDefined();
expect(['Class', 'Constructor']).toContain(box!.targetLabel);
});
it('never emits a CALLS edge to a corrupted qualified/raw name', () => {
const calls = getRelationships(result, 'CALLS');
expect(calls.some((c) => c.target.includes('.') || c.target.includes('new '))).toBe(false);
});
});
describe('C# explicit constructor initializer resolution (F38, mirror of Java #1928)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-explicit-ctor-init'), () => {});
}, 60000);
it('resolves `: base(1)` to the Base type', () => {
const calls = getRelationships(result, 'CALLS');
const baseCall = calls.find((c) => c.target === 'Base');
expect(baseCall).toBeDefined();
expect(baseCall!.source).toBe('Child');
expect(['Class', 'Constructor']).toContain(baseCall!.targetLabel);
});
it('resolves `: this()` to a DISTINCT sibling Child constructor (no self-loop)', () => {
const calls = getRelationships(result, 'CALLS');
const thisCall = calls.find((c) => c.target === 'Child' && c.source === 'Child');
expect(thisCall).toBeDefined();
expect(thisCall!.targetLabel).toBe('Constructor');
expect(thisCall!.rel.sourceId).not.toBe(thisCall!.rel.targetId);
expect(thisCall!.rel.sourceId).toMatch(/Child\.Child#1/);
expect(thisCall!.rel.targetId).toMatch(/Child\.Child#0/);
});
});
describe('C# interface-only `: base()` must not target an interface (#2046)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-interface-only-base'), () => {});
}, 60000);
it('emits no CALLS edge to IFoo from `: base()` on `class C : IFoo`', () => {
const calls = getRelationships(result, 'CALLS');
expect(calls.some((c) => c.target === 'IFoo')).toBe(false);
expect(calls.some((c) => c.targetLabel === 'Interface')).toBe(false);
});
});
describe('C# qualified constructor resolves by qualifier, not same-tail local (#2046)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'csharp-namespace-qualified-ctor'),
() => {},
);
}, 60000);
it('resolves `new B.Foo()` to the Foo in namespace B, not the colliding A.Foo', () => {
const calls = getRelationships(result, 'CALLS');
const hit = calls.find((c) => c.source === 'Make' && c.target === 'Foo');
expect(hit).toBeDefined();
expect(hit!.targetFilePath).toBe('B/Foo.cs');
});
});