diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 11fa8c92c..85b2efd58 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -23,8 +23,8 @@ "_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift \u2014 no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures \u2014 pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture \u2014 pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae." }, "csharp": { - "_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", - "fingerprint": "7e8845040540ae69ef564ebf597305ade31e4480cee1dc910ea0fcfc26794910", + "_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.", + "fingerprint": "701b4274643a9a5ee03a71e9fc0dede28a89524eedb976affc48df72bdd4adcc", "scaling_budget": 1.5 }, "rust": { diff --git a/gitnexus/src/core/ingestion/languages/csharp/receiver-binding.ts b/gitnexus/src/core/ingestion/languages/csharp/receiver-binding.ts index 9cd69b99a..cfc78c5fb 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/receiver-binding.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/receiver-binding.ts @@ -49,18 +49,22 @@ function typeName(typeNode: SyntaxNode): string | null { return typeNode.childForFieldName('name')?.text ?? null; } -/** First entry in the type's `base_list`, read as raw text. C# allows - * generic and qualified bases (`Foo`, `N.M.Base`); we keep the raw - * form so downstream interpret layer can strip generics/qualifiers - * the same way as other type-binding captures. Returns null when the - * type has no base (or an empty base_list). */ +/** First entry in the type's `base_list`, read as raw type text. C# + * allows generic and qualified bases (`Foo`, `N.M.Base`); we keep + * that form so downstream interpretation can strip generics and + * qualifiers consistently. A record primary-constructor base wraps + * its type and constructor arguments in `primary_constructor_base_type`, + * so read its `type` field instead of leaking `(args)` into the binding. + * Returns null when the type has no base (or an empty base_list). */ function firstBaseText(typeNode: SyntaxNode): string | null { for (let i = 0; i < typeNode.namedChildCount; i++) { const child = typeNode.namedChild(i); if (child === null || child.type !== 'base_list') continue; const firstBase = child.namedChild(0); if (firstBase === null) return null; - return firstBase.text; + return firstBase.type === 'primary_constructor_base_type' + ? (firstBase.childForFieldName('type')?.text ?? firstBase.text) + : firstBase.text; } return null; } diff --git a/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json b/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json index f1ea95b55..37ab734d6 100644 --- a/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json @@ -489,7 +489,7 @@ }, "csharp-qualified-base/src/Shapes.cs": { "captureGroups": 38, - "digest": "9f7730c2579cff867f929efc8ad9e314f847b7e020174282c7c104339ad5b610" + "digest": "eb485282a87a35dac553701e6560d26b4e01f585fa36ded4576bbce00c8c163a" }, "csharp-qualified-types/Data/User.cs": { "captureGroups": 7, @@ -512,12 +512,12 @@ "digest": "4f0c929c82e56f41e5b3b4c0aefee2121d7a307ef24535ac998fd7041d0e025b" }, "csharp-record-base/src/Models/BaseEntity.cs": { - "captureGroups": 7, - "digest": "e7da2e190dad718eeaa22dad011740fd2086eb2f86331d887e47cd9c5092003c" + "captureGroups": 8, + "digest": "1ed96b53bfd3a0ed13a7931e82b0b47f4c4acfc23d23f04d4053c7ad994cf9f9" }, "csharp-record-base/src/Models/UserRecord.cs": { - "captureGroups": 10, - "digest": "e6276cd40438125f468adb2f193786a6e888032a3980d0d7ac586d355c40534c" + "captureGroups": 11, + "digest": "416980acf3f3df44c96c375de8cd683031e053b3d82cf1a0ea5ae66b056438b6" }, "csharp-recursive-pattern/Models/Repo.cs": { "captureGroups": 8, diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/BaseEntity.cs b/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/BaseEntity.cs index f744db935..9319dece7 100644 --- a/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/BaseEntity.cs +++ b/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/BaseEntity.cs @@ -1,6 +1,6 @@ namespace Models; -public record BaseEntity +public record BaseEntity(int EntityId) { public virtual bool Save() { return true; } } diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/UserRecord.cs b/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/UserRecord.cs index c13b9fcb2..e60fc2604 100644 --- a/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/UserRecord.cs +++ b/gitnexus/test/fixtures/lang-resolution/csharp-record-base/src/Models/UserRecord.cs @@ -1,6 +1,6 @@ namespace Models; -public record UserRecord : BaseEntity +public record UserRecord(int Id) : BaseEntity(Id) { public override bool Save() { diff --git a/gitnexus/test/unit/scope-resolution/csharp/csharp-captures.test.ts b/gitnexus/test/unit/scope-resolution/csharp/csharp-captures.test.ts index 9fa9bf181..410452fb3 100644 --- a/gitnexus/test/unit/scope-resolution/csharp/csharp-captures.test.ts +++ b/gitnexus/test/unit/scope-resolution/csharp/csharp-captures.test.ts @@ -308,6 +308,61 @@ describe('emitCsharpScopeCaptures — receiver-binding synthesis (`this` / `base expect(baseMatch!['@type-binding.type'].text).toBe('BaseModel'); }); + it('excludes constructor arguments from record primary-constructor base bindings', () => { + const matches = emitCsharpScopeCaptures( + 'record User(int id) : BaseEntity(id) { public void M() { base.Save(); } }', + 'test.cs', + ); + const baseMatch = matches.find( + (m) => '@type-binding.self' in m && m['@type-binding.name'].text === 'base', + ); + expect(baseMatch?.['@type-binding.type'].text).toBe('BaseEntity'); + }); + + it('preserves a plain record base binding', () => { + const matches = emitCsharpScopeCaptures( + 'record User(int id) : BaseEntity { public void M() { base.Save(); } }', + 'test.cs', + ); + const baseMatch = matches.find( + (m) => '@type-binding.self' in m && m['@type-binding.name'].text === 'base', + ); + expect(baseMatch?.['@type-binding.type'].text).toBe('BaseEntity'); + }); + + it('excludes empty constructor arguments from record primary-constructor base bindings', () => { + const matches = emitCsharpScopeCaptures( + 'record User(int id) : BaseEntity() { public void M() { base.Save(); } }', + 'test.cs', + ); + const baseMatch = matches.find( + (m) => '@type-binding.self' in m && m['@type-binding.name'].text === 'base', + ); + expect(baseMatch?.['@type-binding.type'].text).toBe('BaseEntity'); + }); + + it('excludes nested constructor arguments from record primary-constructor base bindings', () => { + const matches = emitCsharpScopeCaptures( + 'record User(int id) : BaseEntity(Create(id, 2)) { public void M() { base.Save(); } }', + 'test.cs', + ); + const baseMatch = matches.find( + (m) => '@type-binding.self' in m && m['@type-binding.name'].text === 'base', + ); + expect(baseMatch?.['@type-binding.type'].text).toBe('BaseEntity'); + }); + + it('preserves qualified generic record primary-constructor base types without arguments', () => { + const matches = emitCsharpScopeCaptures( + 'record User(int id) : App.BaseEntity(id) { public void M() { base.Save(); } }', + 'test.cs', + ); + const baseMatch = matches.find( + (m) => '@type-binding.self' in m && m['@type-binding.name'].text === 'base', + ); + expect(baseMatch?.['@type-binding.type'].text).toBe('App.BaseEntity'); + }); + it('does not emit `this` or `base` for static methods', () => { const matches = emitCsharpScopeCaptures('class User { public static void M() { } }', 'test.cs'); const receiverMatches = matches.filter((m) => '@type-binding.self' in m);