mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-25 05:53:44 +00:00
* fix(csharp): bind nullable field types for receiver resolution (#3498) A C# field written `User? repo;` got no type binding. Its type is a `nullable_type` node, and the field_declaration rules only matched identifier, generic_name and qualified_name types. Parameters, properties, method returns and foreach variables already have a nullable_type rule, so fields were the odd one out: the class scope had no binding for `repo`, and `this.repo.Save()` was dropped as receiver-unresolved. Add the missing field rule. interpretCsharpTypeBinding already strips the `?`, so the field binds `User`, as the property spelling does. Shape arm: csharp `decoratedFieldType` moves VISIBLE-GAP -> RESOLVES, with the edge on `User.Save`. No corpus fixture declares a nullable field, so the count arm and every other cell are unchanged, and the corpus edge (2838) and drop (192) sets match main exactly. Scope-capture fingerprints are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(csharp): preserve nullable field ownership * fix(csharp): refuse unowned nullable types * test(csharp): guard nullable-member fixture cleanup (#3664) Address nexus-check review on resolvers/csharp.test.ts:1471: skip the temp-root removal when mkdtempSync never ran, so a setup failure is not masked by a secondary rmSync(undefined) TypeError. Matches the guard in csharp-nullable-field-review.test.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(bench): scope nullable wrapper claim to what the code does (#3664) The ownership-controls note said the nullable-field marker preserves custom generic wrappers. Its only control, Custom.List<User>?, passes through the qualified-type refusal instead. State that unqualified wrappers keep the wrapper type (no native control yet) and that qualified spellings, custom wrappers included, stay unbound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: preserve C# integration notes * fix(csharp): preserve generic argument qualifiers Ignore comments when checking outer nullable-field qualification. Refs #3664 --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
36e16434ea
commit
af5fd9665c
10 changed files with 399 additions and 6 deletions
|
|
@ -1,5 +1,15 @@
|
|||
# Receiver-resolution baseline
|
||||
|
||||
## C# nullable field types (#3498)
|
||||
|
||||
A field written `User? repo;` has a `nullable_type` as its declared type.
|
||||
Parameters, properties, method returns and foreach variables already had a
|
||||
`nullable_type` binding rule, but fields did not, so the class scope held no
|
||||
binding for `repo` and `this.repo.Save()` was dropped. With the new field rule,
|
||||
`csharp.decoratedFieldType` moves from VISIBLE-GAP to RESOLVES, and the edge
|
||||
lands on `User.Save`. No corpus fixture declares a nullable field, so the count
|
||||
arm and every other shape cell are unchanged.
|
||||
|
||||
## PHP namespaced-function factory results (#3498 follow-up)
|
||||
|
||||
PHP types results of namespaced function calls such as `rp\make()`,
|
||||
|
|
@ -1110,10 +1120,50 @@ The re-measured combined baseline is 139 call drops, 31 reads, no writes and
|
|||
|
||||
Main `2b4456553` is merged normally. The field lookup retains both the declared namespace purpose and provider-qualified type handling. Build and both TypeScript configurations pass, and all 59 focused C++ and CommonJS merge checks pass. The measured receiver baseline has 133 call drops, 160 total drops, 27 reads and 0 writes.
|
||||
|
||||
## Nullable field ownership controls
|
||||
|
||||
Unqualified nullable wrapper fields such as `List<User>?` keep the wrapper as
|
||||
their type instead of unwrapping to `User`; no native control covers that
|
||||
shape. Qualified nullable fields, including custom wrappers such as
|
||||
`Custom.List<User>?`, remain unsupported: their type binding is refused
|
||||
until namespace-aware lookup can establish the written owner. Both external
|
||||
and in-workspace qualified spellings record unresolved call drops instead of
|
||||
borrowing a workspace-unique tail name. Local User? fields still resolve.
|
||||
|
||||
The first mixed fixture masked a unique-name fallback by declaring several
|
||||
User classes. An isolated one-User fixture reproduces that false edge. The
|
||||
correction passes 240 native C# tests in three files, including distinct
|
||||
negative repos, a local positive and explicit receiver-unresolved receipts.
|
||||
A separate qualified-positive probe exposed the existing unsupported shape;
|
||||
that case is documented as a limit, not claimed as a repaired positive.
|
||||
The 5,323-record corpus is byte-identical to main 2b4456553, and the receiver
|
||||
benchmark retains 133 calls, 160 drops of all kinds, 27 reads and no writes.
|
||||
The earlier 273-test run predates this last boundary correction.
|
||||
|
||||
### After merging #3653 into the C# namespace branch
|
||||
|
||||
Main `2b4456553` is merged normally. The measured baseline has 130 call drops and 156 across all kinds, with 26 reads and zero writes. Main has 133 calls, 27 reads and zero writes (160 total). Every shape state matches main. The fresh build and both TypeScript configurations pass, as do all 68 namespace-boundary, capture, hook and CommonJS merge checks.
|
||||
|
||||
After merging main caacc3bac normally, 321 tests across six C# ownership, resolver, extractor, golden and namespace-precedence files pass. Package build, both pinned TypeScript configurations and lint pass (zero errors, seven existing warnings). The C# capture check retains fingerprint 28a51b97c32bf95d85e5a322e443ab00cd490a1178269c2f6f9cfc4fb9776686: 186 files, 2,841 groups, ratio 1.109 within the unchanged 1.5 budget. The corpus is byte-identical to main with 5,328 records. Receiver totals are 130 calls, 156 all kinds, 26 reads and zero writes; only csharp.decoratedFieldType moves VISIBLE-GAP to RESOLVES. Qualified nullable fields remain unsupported as described above.
|
||||
|
||||
After merging main c30449394 normally, 324 tests across the same six files pass. Package build, both pinned TypeScript configurations and lint (zero errors) pass. All 16 scope-capture checks pass against main's baselines; C# keeps fingerprint 3720a3d2 with 193 files, 2,899 groups and ratio 1.142 within the unchanged 1.5 budget. The 5,501-record corpus is byte-identical to main. The regenerated receiver baseline keeps main's 132 calls, 160 all kinds, 28 reads and zero writes, with origins 30 external, 45 unknown, 55 in-program and 2 unclassified; only csharp.decoratedFieldType moves VISIBLE-GAP to RESOLVES.
|
||||
|
||||
### C# nullable fields with the prepared PHP parent
|
||||
|
||||
The normal merge of PHP parent `6149ce156` includes Kotlin parent `d5a7c73fa`.
|
||||
The three C# implementation changes remain identical to the reviewed `8b986c68a`
|
||||
source. Package build, both pinned TypeScript configurations and 324 tests in
|
||||
six files pass. The C# capture check retains fingerprint `3720a3d2`, 193 entries
|
||||
and 2,899 groups, with ratio 1.022 below the unchanged 1.5 budget. The earlier
|
||||
main-only integration's all-16-language capture run is not repeated here.
|
||||
|
||||
The corpus contains 5,559 records, byte-identical to the PHP parent's corpus.
|
||||
Receiver update and check agree on 138 call drops, 168 total, 30 reads and no
|
||||
writes, also matching that parent. Origins are 57 in-program, 49 unknown,
|
||||
30 external and two unclassified. Only `csharp.decoratedFieldType` changes
|
||||
from VISIBLE-GAP to RESOLVES relative to the parent. Final integration with
|
||||
current main and exact-head hosted CI remain required.
|
||||
|
||||
After merging main caacc3bac normally, totals remain 130 calls, 156 across all kinds, 26 reads and zero writes. Origin labels are 30 external, 45 unknown, 53 in-program and 2 unclassified. Shape states are unchanged. The 5,328-record corpus is byte-identical to current main. Package build, both pinned TypeScript configurations, 23 CommonJS/C# precedence tests and 15 library-origin boundary tests pass.
|
||||
|
||||
### Same-package inheritance after #3655
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
"indexElement": "VISIBLE-GAP",
|
||||
"fieldReceiverCall": "RESOLVES",
|
||||
"decoratedReceiverBase": "N/A",
|
||||
"decoratedFieldType": "VISIBLE-GAP"
|
||||
"decoratedFieldType": "RESOLVES"
|
||||
},
|
||||
"ruby": {
|
||||
"plainChain": "VISIBLE-GAP",
|
||||
|
|
|
|||
|
|
@ -77,10 +77,28 @@ export function interpretCsharpTypeBinding(captures: CaptureMatch): ParsedTypeBi
|
|||
const typeCap = captures['@type-binding.type'];
|
||||
if (nameCap === undefined || typeCap === undefined) return null;
|
||||
|
||||
// Strip nullable suffix (`User?` → `User`), single-arg generic wrapper
|
||||
// (`List<User>` → `User`), and qualifier (`System.User` → `User`) so
|
||||
// receiver-typed resolution treats these identically.
|
||||
const rawType = stripQualifier(stripGeneric(stripNullable(typeCap.text.trim())));
|
||||
// Other bindings retain their existing nullable, wrapper and qualifier
|
||||
// normalization. Nullable field bindings keep wrappers as receiver types.
|
||||
const withoutNullable = stripNullable(typeCap.text.trim());
|
||||
// Qualified nullable fields need namespace-aware type lookup. The generic
|
||||
// receiver fallback can otherwise borrow an unrelated unique tail name.
|
||||
// Leave these unbound until the provider can establish the written owner.
|
||||
if (captures['@type-binding.nullable-field'] !== undefined) {
|
||||
// Comments can contain punctuation that is not part of the type.
|
||||
const qualificationText = withoutNullable.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, '');
|
||||
let depth = 0;
|
||||
for (const char of qualificationText) {
|
||||
if (char === '<') depth++;
|
||||
else if (char === '>') depth = Math.max(0, depth - 1);
|
||||
// A qualifier inside a generic argument does not qualify the receiver.
|
||||
else if (char === '.' && depth === 0) return null;
|
||||
}
|
||||
}
|
||||
// A nullable wrapper field is not its element: List<User> is not User.
|
||||
const rawType =
|
||||
captures['@type-binding.nullable-field'] !== undefined
|
||||
? withoutNullable
|
||||
: stripQualifier(stripGeneric(withoutNullable));
|
||||
|
||||
// Anchor captures distinguish the source of the binding. Order
|
||||
// matters: more-specific anchors take precedence.
|
||||
|
|
|
|||
|
|
@ -319,6 +319,14 @@ const CSHARP_SCOPE_QUERY = `
|
|||
(variable_declarator
|
||||
name: (identifier) @type-binding.name))) @type-binding.annotation
|
||||
|
||||
;; \`private User? _user;\` — same nullable_type shape that parameters and
|
||||
;; properties already bind. Preserve this field's written type ownership.
|
||||
(field_declaration
|
||||
(variable_declaration
|
||||
type: (nullable_type) @type-binding.type
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name))) @type-binding.annotation @type-binding.nullable-field
|
||||
|
||||
;; Type bindings — property declaration: \`public User Owner { get; set; }\`.
|
||||
(property_declaration
|
||||
type: (identifier) @type-binding.type
|
||||
|
|
|
|||
|
|
@ -1937,6 +1937,7 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
|
|||
'@import.publishes',
|
||||
'@type-binding.name',
|
||||
'@type-binding.type',
|
||||
'@type-binding.nullable-field',
|
||||
'@reference.name',
|
||||
'@reference.qualified-name',
|
||||
// The generic arguments a heritage base was written with, when the emitter's
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
writeFixtureRepo,
|
||||
type PipelineResult,
|
||||
} from './resolvers/helpers.js';
|
||||
|
||||
describe('nullable generic field receiver ownership', () => {
|
||||
let root: string | undefined;
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-csharp-generic-field-'));
|
||||
writeFixtureRepo(root, {
|
||||
'Wrapper.cs': 'class List<T> { public void Save() {} }',
|
||||
'Outer.cs': 'class Outer<T> { public void Save() {} }',
|
||||
'Inner.cs': 'class Inner<T> { public void Save() {} }',
|
||||
'User.cs': `class User { public void Save() {} }
|
||||
namespace Models { class User { public void Save() {} } }`,
|
||||
'Host.cs': `class Host {
|
||||
List<User>? plain;
|
||||
List<Models.User>? dotted;
|
||||
Custom.List<User>? qualified;
|
||||
Outer<User>.Inner<Models.User>? nestedOuter;
|
||||
List</* > */Models.User>? commented;
|
||||
List<// >
|
||||
Models.User>? lineCommented;
|
||||
Outer</* < */User>.Inner<User>? commentedOuter;
|
||||
Outer<User>/* < */.Inner<User>? commentedSuffix;
|
||||
void PlainCall() { this.plain.Save(); }
|
||||
void DottedCall() { this.dotted.Save(); }
|
||||
void QualifiedCall() { this.qualified.Save(); }
|
||||
void NestedOuterCall() { this.nestedOuter.Save(); }
|
||||
void CommentedCall() { this.commented.Save(); }
|
||||
void LineCommentedCall() { this.lineCommented.Save(); }
|
||||
void CommentedOuterCall() { this.commentedOuter.Save(); }
|
||||
void CommentedSuffixCall() { this.commentedSuffix.Save(); }
|
||||
}`,
|
||||
});
|
||||
result = await runPipelineFromRepo(root, () => {}, {});
|
||||
}, 240000);
|
||||
|
||||
afterAll(() => {
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const saves = (source: string) =>
|
||||
getRelationships(result, 'CALLS')
|
||||
.filter((edge) => edge.source === source && edge.target === 'Save')
|
||||
.map((edge) => edge.targetFilePath);
|
||||
|
||||
it('keeps an unqualified wrapper as the receiver', () => {
|
||||
expect(saves('PlainCall')).toEqual(['Wrapper.cs']);
|
||||
});
|
||||
|
||||
it('keeps qualification inside the argument without selecting User.Save', () => {
|
||||
expect(saves('DottedCall')).toEqual(['Wrapper.cs']);
|
||||
});
|
||||
|
||||
it('still declines a qualified outer wrapper', () => {
|
||||
expect(saves('QualifiedCall')).toEqual([]);
|
||||
});
|
||||
|
||||
it('still declines qualification after a generic outer type', () => {
|
||||
expect(saves('NestedOuterCall')).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores a block comment when classifying an argument-qualified wrapper', () => {
|
||||
expect(saves('CommentedCall')).toEqual(['Wrapper.cs']);
|
||||
});
|
||||
|
||||
it('ignores a line comment when classifying an argument-qualified wrapper', () => {
|
||||
expect(saves('LineCommentedCall')).toEqual(['Wrapper.cs']);
|
||||
});
|
||||
|
||||
it('does not borrow standalone Outer or Inner through an argument comment', () => {
|
||||
expect(saves('CommentedOuterCall')).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not borrow standalone Outer or Inner through a suffix comment', () => {
|
||||
expect(saves('CommentedSuffixCall')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getRelationships,
|
||||
getResolutionOutcomes,
|
||||
runPipelineFromRepo,
|
||||
writeFixtureRepo,
|
||||
} from './resolvers/helpers.js';
|
||||
|
||||
describe('C# nullable field isolated owner boundary', () => {
|
||||
it.each([
|
||||
['external qualified owner', 'External.User?', '', false],
|
||||
['local nullable owner', 'User?', '', true],
|
||||
[
|
||||
'workspace qualified owner remains unsupported',
|
||||
'Actual.User?',
|
||||
'namespace Actual { public class User { public void Save() {} } }',
|
||||
false,
|
||||
],
|
||||
] as const)(
|
||||
'%s',
|
||||
async (_label, type, qualifiedDefinition, expected) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-csharp-isolated-owner-'));
|
||||
try {
|
||||
writeFixtureRepo(root, {
|
||||
'Models.cs': `${qualifiedDefinition}
|
||||
public class User { public void Save() {} }
|
||||
public class Decoy { public void Save() {} }
|
||||
public class Host { private ${type} repo; public void Probe() { this.repo.Save(); } }
|
||||
`,
|
||||
});
|
||||
const result = await runPipelineFromRepo(root, () => {}, {});
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.source === 'Probe' && e.target === 'Save',
|
||||
);
|
||||
expect(calls).toHaveLength(expected ? 1 : 0);
|
||||
if (expected) expect(calls[0]!.rel.targetId).toContain('User.Save');
|
||||
else
|
||||
expect(
|
||||
getResolutionOutcomes(result).some(
|
||||
(outcome) =>
|
||||
outcome.kind === 'suppressed' &&
|
||||
outcome.name === 'Save' &&
|
||||
outcome.reason === 'receiver-unresolved',
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
120000,
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { beforeAll, afterAll, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
writeFixtureRepo,
|
||||
type PipelineResult,
|
||||
} from './resolvers/helpers.js';
|
||||
describe('nullable field receiver ownership', () => {
|
||||
let root: string;
|
||||
let result: PipelineResult;
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-csharp-nullable-'));
|
||||
writeFixtureRepo(root, {
|
||||
'Foreign.cs': `namespace ForeignCaller {
|
||||
class User { public void Save() {} }
|
||||
class Decoy { public void Save() {} }
|
||||
class Host { External.User? repo; void ForeignCall(){this.repo.Save();} }
|
||||
}`,
|
||||
'Generic.cs': `namespace Custom { class List<T> {} }
|
||||
namespace GenericCaller {
|
||||
class User { public void Save() {} }
|
||||
class Decoy { public void Save() {} }
|
||||
class Host { Custom.List<User>? repo; void GenericCall(){this.repo.Save();} }
|
||||
}`,
|
||||
'Local.cs': `namespace LocalCaller {
|
||||
class User { public void Save() {} }
|
||||
class Host { User? repo; void LocalCall(){this.repo.Save();} }
|
||||
}`,
|
||||
});
|
||||
result = await runPipelineFromRepo(root, () => {}, {});
|
||||
}, 120000);
|
||||
afterAll(() => {
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const saves = (source: string) =>
|
||||
getRelationships(result, 'CALLS')
|
||||
.filter((e) => e.source === source && e.target === 'Save')
|
||||
.map((e) => e.targetFilePath);
|
||||
it('keeps a failed qualified nullable type unresolved', () => {
|
||||
expect(saves('ForeignCall')).toEqual([]);
|
||||
});
|
||||
it('does not unwrap a nullable custom generic wrapper to its argument', () => {
|
||||
expect(saves('GenericCall')).toEqual([]);
|
||||
});
|
||||
it('still binds the local nullable class field', () => {
|
||||
expect(saves('LocalCall')).toEqual(['Local.cs']);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* C#: heritage resolution via base_list + ambiguous namespace-import refusal
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
|
|
@ -1417,6 +1417,80 @@ describe('Field type resolution (C#)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nullable member types as call receivers (#3498)
|
||||
//
|
||||
// `User? repo;` is a field_declaration whose variable_declaration type is a
|
||||
// `nullable_type`. Parameters and properties had a nullable_type binding rule;
|
||||
// fields did not, so the class scope held no binding for `repo` and
|
||||
// `this.repo.Save()` was dropped as receiver-unresolved. `Address` and `User`
|
||||
// both declare `Save`, so a by-name fallback cannot pass these rows.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# nullable member types as call receivers (#3498)', () => {
|
||||
let result: PipelineResult;
|
||||
let root: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-csharp-nullable-members-'));
|
||||
writeFixtureRepo(root, {
|
||||
'Address.cs': `public class Address {
|
||||
public void Save() {}
|
||||
}
|
||||
`,
|
||||
'User.cs': `public class User {
|
||||
public void Save() {}
|
||||
}
|
||||
`,
|
||||
'Holders.cs': `class NullableFieldHolder {
|
||||
User? repo;
|
||||
void ViaNullableField() {
|
||||
this.repo.Save();
|
||||
}
|
||||
}
|
||||
|
||||
class NullablePropertyHolder {
|
||||
User? Repo { get; set; }
|
||||
void ViaNullableProperty() {
|
||||
this.Repo.Save();
|
||||
}
|
||||
}
|
||||
|
||||
class PlainFieldHolder {
|
||||
User repo;
|
||||
void ViaPlainField() {
|
||||
this.repo.Save();
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
result = await runPipelineFromRepo(root, () => {});
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => {
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ caller: 'ViaNullableField', spelling: 'User? repo;' },
|
||||
{ caller: 'ViaNullableProperty', spelling: 'User? Repo { get; set; }' },
|
||||
{ caller: 'ViaPlainField', spelling: 'User repo; (control)' },
|
||||
])('$caller ($spelling) calls User.Save only', ({ caller }) => {
|
||||
const targets = getRelationships(result, 'CALLS')
|
||||
.filter((edge) => edge.source === caller && edge.target === 'Save')
|
||||
.map((edge) => `${edge.targetLabel}:${edge.targetFilePath}`);
|
||||
expect(targets).toEqual(['Method:User.cs']);
|
||||
});
|
||||
|
||||
it('records no receiver-unresolved drop in the holders', () => {
|
||||
const drops = getResolutionOutcomes(result)
|
||||
.filter((outcome) => outcome.kind === 'suppressed')
|
||||
.filter((outcome) => outcome.filePath === 'Holders.cs')
|
||||
.map((outcome) => `${outcome.reason}:${outcome.name}:${outcome.range.startLine}`);
|
||||
expect(drops).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 8A: Deep field chain resolution (3-level)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import { interpretCsharpTypeBinding } from '../../../../src/core/ingestion/languages/csharp/interpret.js';
|
||||
|
||||
const range = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 } as const;
|
||||
const capture = (name: string, text: string): Capture => ({ name, text, range });
|
||||
|
||||
function nullableField(type: string) {
|
||||
const captures: CaptureMatch = {
|
||||
'@type-binding.name': capture('@type-binding.name', 'field'),
|
||||
'@type-binding.type': capture('@type-binding.type', type),
|
||||
'@type-binding.annotation': capture('@type-binding.annotation', type),
|
||||
'@type-binding.nullable-field': capture('@type-binding.nullable-field', type),
|
||||
};
|
||||
return interpretCsharpTypeBinding(captures);
|
||||
}
|
||||
|
||||
describe('nullable field outer qualification', () => {
|
||||
it.each([
|
||||
'List<User>',
|
||||
'List<Models.User>',
|
||||
'Dictionary<string, Models.User>',
|
||||
'List<Dictionary<string, Models.User>>',
|
||||
'Box<Models.Outer<int>.Inner<string>>',
|
||||
'List</* > */Models.User>',
|
||||
'List<// >\nModels.User>',
|
||||
])('preserves the entire unqualified wrapper %s', (type) => {
|
||||
expect(nullableField(`${type}?`)).toEqual({
|
||||
boundName: 'field',
|
||||
rawTypeName: type,
|
||||
source: 'annotation',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
'Actual.User',
|
||||
'External.User',
|
||||
'Custom.List<User>',
|
||||
'Custom.List<Models.User>',
|
||||
'Outer<User>.Inner<Models.User>',
|
||||
'Outer<List<Models.User>>.Inner<User>',
|
||||
'Outer<User>.Values',
|
||||
'Outer</* < */User>.Inner<User>',
|
||||
'Outer<User>/* < */.Inner<User>',
|
||||
'Outer<User>// <\n.Inner<User>',
|
||||
])('declines a qualified outer receiver %s', (type) => {
|
||||
expect(nullableField(`${type}?`)).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue