GitNexus/gitnexus/test/unit/scope-resolution/csharp/csharp-interpret.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

82 lines
3.4 KiB
TypeScript

/**
* Coverage for `interpretCsharpTypeBinding` type normalization, focused on the
* F41 analog (#1928): the qualifier strip must not reach into generic type
* ARGUMENTS. `Dictionary<string, Ns.User>` was corrupted into `User>` by an
* unguarded `lastIndexOf('.')`. Multi-arg generics must stay intact so the
* collection-accessor (`.Values`/`.Keys`) unwrap keeps working.
*/
import { describe, it, expect } from 'vitest';
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { interpretCsharpTypeBinding } from '../../../../src/core/ingestion/languages/csharp/interpret.js';
const ZERO = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 } as const;
const cap = (name: string, text: string): Capture => ({ name, text, range: ZERO });
function raw(typeText: string): string | undefined {
const m: CaptureMatch = {
'@type-binding.name': cap('@type-binding.name', 'x'),
'@type-binding.type': cap('@type-binding.type', typeText),
'@type-binding.annotation': cap('@type-binding.annotation', typeText),
};
return interpretCsharpTypeBinding(m)?.rawTypeName;
}
describe('interpretCsharpTypeBinding — type normalization (F41 analog #1928)', () => {
it('does not corrupt a qualified generic TYPE ARGUMENT (the bug)', () => {
// Was `User>` before the fix.
expect(raw('Dictionary<string, Ns.User>')).toBe('Dictionary<string, Ns.User>');
});
it('leaves an unqualified multi-arg generic intact (collection-accessor unwrap)', () => {
expect(raw('Dictionary<string, Widget>')).toBe('Dictionary<string, Widget>');
});
it('strips the OUTER qualifier of a generic while keeping the type args', () => {
expect(raw('Ns.Dictionary<string, User>')).toBe('Dictionary<string, User>');
});
it('unwraps a single-arg known container to its (qualified) element type', () => {
expect(raw('List<Ns.User>')).toBe('User');
expect(raw('List<User>')).toBe('User');
expect(raw('Task<User>')).toBe('User');
});
it('strips a plain qualifier', () => {
expect(raw('Ns.User')).toBe('User');
expect(raw('A.B.User')).toBe('User');
});
it('strips a nullable suffix', () => {
expect(raw('User?')).toBe('User');
});
it('unwraps a nullable single-arg generic (`List<User>?` → `User`)', () => {
expect(raw('List<User>?')).toBe('User');
});
it('does not corrupt a nested generic — keeps it intact (no `>>` artifact)', () => {
expect(raw('List<Dictionary<string, User>>')).toBe('List<Dictionary<string, User>>');
});
it('strips the outer qualifier of an unrecognized generic, keeping its args', () => {
// Accepted limitation: unknown generics are not erased to the bare base —
// only the OUTER qualifier is removed; the generic suffix is preserved.
expect(raw('Ns.Box<User>')).toBe('Box<User>');
});
it('passes through a plain simple type', () => {
expect(raw('User')).toBe('User');
});
it('preserves a collection-accessor suffix (`data.Values`)', () => {
expect(raw('data.Values')).toBe('data.Values');
});
it('strips nested types through a generic outer (`Ns.Outer<int>.Inner` → `Inner`)', () => {
// Prior generic-aware strip sliced at the first `<` and regressed to
// `Outer<int>.Inner` (unresolvable). Last `.` at bracket depth 0 fixes both
// this shape and the F41 `Dictionary<string, Ns.User>` case (#2046 P3).
expect(raw('Ns.Outer<int>.Inner')).toBe('Inner');
expect(raw('Outer.Inner')).toBe('Inner');
});
});