From 3375beec8910d4f3363f6df41e3196a7fb7323a5 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 14:29:05 +0000 Subject: [PATCH 01/10] fix(rust): strip dyn keyword when normalizing trait-object type names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeRustTypeName/normalizeRustReturnType stripped reference sigils, pointer sigils, and smart-pointer wrappers but never the `dyn` keyword, so a `&dyn Trait`-typed receiver normalized to the literal string "dyn Trait" instead of "Trait" — an unmatchable name that silently broke every downstream receiver-type lookup for trait-object dispatch. Part of the #2604 fix (root cause has a second, independent half: abstract trait methods are invisible to scope resolution until function_signature_item is captured — next commit). --- .../core/ingestion/languages/rust/interpret.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gitnexus/src/core/ingestion/languages/rust/interpret.ts b/gitnexus/src/core/ingestion/languages/rust/interpret.ts index a53a6e1c2..73ecd264e 100644 --- a/gitnexus/src/core/ingestion/languages/rust/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/rust/interpret.ts @@ -2,8 +2,23 @@ import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'git const REF_PREFIX_RE = /^&\s*(mut\s+)?/; const PTR_PREFIX_RE = /^\*\s*(const|mut)?\s*/; +const DYN_PREFIX_RE = /^dyn\s+/; const ENUM_VARIANT_NAMES = new Set(['Some', 'None', 'Ok', 'Err']); +// `dyn Trait`, `&dyn Trait`, `Box` all name a trait object whose +// receiver-dispatch target is the trait itself (#2604) — strip the `dyn` +// keyword and any auto-trait/lifetime bound list (`dyn Trait + Send`) down to +// the principal trait name. Reference/pointer sigils are stripped by the +// caller first; wrapper unwrapping (Box etc.) runs before this so the +// unwrapped inner text still gets the same treatment. +function stripDynBound(t: string): string { + if (!DYN_PREFIX_RE.test(t)) return t; + t = t.replace(DYN_PREFIX_RE, ''); + const plus = t.indexOf('+'); + if (plus !== -1) t = t.slice(0, plus); + return t.trim(); +} + // ─── interpretImport ────────────────────────────────────────────────────── export function interpretRustImport(captures: CaptureMatch): ParsedImport | null { @@ -98,6 +113,7 @@ export function normalizeRustTypeName(text: string): string { const inner = extractFirstGenericArg(t); if (inner !== null) t = inner; } + t = stripDynBound(t); const bracket = t.indexOf('<'); if (bracket !== -1) t = t.slice(0, bracket); // Take last segment of qualified paths (crate::foo::Bar → Bar) @@ -158,6 +174,7 @@ function normalizeRustReturnType(text: string): string { } } } + t = stripDynBound(t); const bracket = t.indexOf('<'); if (bracket !== -1) t = t.slice(0, bracket); const lastColon = t.lastIndexOf('::'); From 57db7bc166e332b6d0a981bf6a3dd8b974356612 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 14:29:37 +0000 Subject: [PATCH 02/10] fix(rust): capture abstract trait methods for scope resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fn foo(&self) -> T; (no body) parses as function_signature_item, a grammar node distinct from function_item that RUST_SCOPE_QUERY never captured. An abstract trait method therefore had no Function scope and no declaration, so populateClassOwnedMembers never wired its ownerId to the trait's Class scope — invisible to the CALLS-edge receiver-bound resolution pass even after a receiver's type resolves to the trait correctly. Together with the previous commit's dyn-stripping fix, a call through a &dyn Trait parameter now emits a CALLS edge to the trait's method (#2604). --- gitnexus/src/core/ingestion/languages/rust/query.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gitnexus/src/core/ingestion/languages/rust/query.ts b/gitnexus/src/core/ingestion/languages/rust/query.ts index a0e75f0fa..bef3f1bd7 100644 --- a/gitnexus/src/core/ingestion/languages/rust/query.ts +++ b/gitnexus/src/core/ingestion/languages/rust/query.ts @@ -10,6 +10,7 @@ const RUST_SCOPE_QUERY = ` (enum_item) @scope.class (union_item) @scope.class (function_item) @scope.function +(function_signature_item) @scope.function (closure_expression) @scope.function (block) @scope.block (if_expression) @scope.block @@ -55,6 +56,14 @@ const RUST_SCOPE_QUERY = ` (function_item name: (identifier) @declaration.name) @declaration.function +;; Declarations — trait method signature (required method, no body, +;; e.g. fn foo(self) -> T; inside a trait body). Without this, an abstract +;; trait method is invisible to scope resolution — never owned by its +;; trait's Class scope, so a dyn Trait receiver can never dispatch to +;; it (#2604). +(function_signature_item + name: (identifier) @declaration.name) @declaration.function + ;; Declarations — struct fields (field_declaration name: (field_identifier) @declaration.name From 052319c9ccd80a6a85b9bbe4b9779e696feb9abc Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 14:48:54 +0000 Subject: [PATCH 03/10] test(rust): add regression coverage for trait-object dispatch (#2604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New minimal fixture (single trait + impl + &dyn Trait call site, no other same-named callers) proves the dyn-dispatch CALLS edge discriminates: fails against the pre-fix source (0 edges) and passes against the two preceding commits' fix (exactly 1 edge, verified via the CLI analyze pipeline against a standalone repo). The existing rust-abstract-dispatch fixture was NOT extended for this, deliberately: it already has other callers referencing the same method names (process()'s repo.find()/save()/count()), and an existing resolution fallback picks those up via simple-name matching regardless of receiver type — masking this specific defect in the in-process test-pipeline path. A dedicated, single-caller fixture keeps the regression test load-bearing. --- .../rust-dyn-trait-object/src/lib.rs | 15 ++++++++++++ .../test/integration/resolvers/rust.test.ts | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dyn-trait-object/src/lib.rs diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dyn-trait-object/src/lib.rs b/gitnexus/test/fixtures/lang-resolution/rust-dyn-trait-object/src/lib.rs new file mode 100644 index 000000000..d25dad83d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dyn-trait-object/src/lib.rs @@ -0,0 +1,15 @@ +pub trait Behaviour { + fn trait_target(&self) -> u32; +} + +pub struct Impl1; + +impl Behaviour for Impl1 { + fn trait_target(&self) -> u32 { + 7 + } +} + +pub fn calls_via_dyn(b: &dyn Behaviour) -> u32 { + b.trait_target() +} diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index 9cd4f94e8..87aa6899b 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -1951,6 +1951,29 @@ describe('Rust abstract dispatch (Repository trait)', () => { }); }); +// --------------------------------------------------------------------------- +// #2604: trait-object (&dyn Trait) receiver dispatch +// --------------------------------------------------------------------------- + +describe('Rust dyn trait-object dispatch (#2604)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-dyn-trait-object'), () => {}); + }, 60000); + + it('detects Impl1 struct and Behaviour trait', () => { + expect(getNodesByLabel(result, 'Struct')).toContain('Impl1'); + expect(getNodesByLabel(result, 'Trait')).toContain('Behaviour'); + }); + + it('emits exactly one CALLS edge from calls_via_dyn(b: &dyn Behaviour) to trait_target', () => { + const calls = getRelationships(result, 'CALLS'); + const dynCalls = calls.filter((c) => c.source === 'calls_via_dyn' && c.target === 'trait_target'); + expect(dynCalls.length).toBe(1); + }); +}); + // --------------------------------------------------------------------------- // SM-11: Rust Child extends Parent — qualified-syntax MRO // From 881c6bccc7b082eb4b44ceff0b6d30ca4a1eaa4e Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 14:49:45 +0000 Subject: [PATCH 04/10] test(rust): regenerate captures golden snapshot for function_signature_item Expected drift from the query.ts change: abstract trait methods now emit a scope + declaration capture, shifting captureGroups/digest for every rust-* fixture containing a trait with a required (bodyless) method. --- .../expected-captures.json | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json index d977d99bf..f632f78aa 100644 --- a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json @@ -1,7 +1,7 @@ { "rust-abstract-dispatch/src/lib.rs": { - "captureGroups": 30, - "digest": "88309004d1ab00054f81bc55c1d058fc4ca25781162d6b94b7e7ce631a5d61b2" + "captureGroups": 34, + "digest": "973679363065ecd54c4e5128a9fab214ea27eca24f0f079c63a3c6f285e678b0" }, "rust-abstract-dispatch/src/main.rs": { "captureGroups": 21, @@ -148,8 +148,8 @@ "digest": "e0120e3f215282e68d83b4f8f5d8918945e0b3e7ce4e0128c6afd2aa43caa1c0" }, "rust-cross-module-collision/src/traits.rs": { - "captureGroups": 3, - "digest": "88eef9d92ea6e370bd8ef7fbf64c42ec622ec933fb53c56b32bc67db87fa8e03" + "captureGroups": 5, + "digest": "c7150a5052e0e2b5fd7fc21cc8ce361530fe5cc60f67ad0e2ef945bb97965b53" }, "rust-deep-field-chain/models.rs": { "captureGroups": 24, @@ -171,6 +171,10 @@ "captureGroups": 22, "digest": "c53db401a81fde2ffd5665393acb9cd605a62ec51c015c3aafb3f41c0897471f" }, + "rust-dyn-trait-object/src/lib.rs": { + "captureGroups": 23, + "digest": "720618dff6a43ab8e5b59aa354c0c448b9057dd6f2f7b3b22b13b82d53745943" + }, "rust-err-unwrap/src/error.rs": { "captureGroups": 9, "digest": "798c8e01c6e54792ba69e845248efc8abf0cba38fa3d16fb8e0d1f6dd2ad2b7e" @@ -316,8 +320,8 @@ "digest": "cd836a2a9c15ab240961d2e15f192f7e33d65eb5ebf2e1a8af2f620a47fe66ae" }, "rust-method-enrichment/src/lib.rs": { - "captureGroups": 40, - "digest": "71627a8218e32514b6945e4e310686eb631ce37451c90c9644e3f5336a37820b" + "captureGroups": 42, + "digest": "a4d9ca570fbb1ff1859a0b4f737aa3507b236f99700d37ded2c8c36518add567" }, "rust-method-enrichment/src/main.rs": { "captureGroups": 18, @@ -360,8 +364,8 @@ "digest": "141388068614e16d96f27cfdf18ac9001b9e202ce832fe10f38dab990637b3ab" }, "rust-parent-resolution/src/serializable.rs": { - "captureGroups": 3, - "digest": "f35d44f44d81e3a0be40f68ba9dbd4bde6f01659fa15b6db34a458ad460f904e" + "captureGroups": 5, + "digest": "f33bb881dd937cdd5af2eca6b0284ea79ca2d296c79217513f882dcba8a82fd8" }, "rust-parent-resolution/src/user.rs": { "captureGroups": 13, @@ -372,8 +376,8 @@ "digest": "bc8946d31db81b85d780633608fdaa7565258cd788285fa00cd6dcb0de3dd16c" }, "rust-qualified-trait/src/traits.rs": { - "captureGroups": 5, - "digest": "15be069f28f1400e4beb0b0860acb59979f78549960486f36a92f56578f05a06" + "captureGroups": 9, + "digest": "10f3bba4c2a16cdac77de0498ff506910ac09cc1a85e7daebfc5743c54e26015" }, "rust-qualified-trait/src/widget.rs": { "captureGroups": 23, @@ -492,12 +496,12 @@ "digest": "f1b9f72d74467be55a8b7679215b49bcabb4d0fced6080f752672070b32ed93d" }, "rust-traits/src/traits/clickable.rs": { - "captureGroups": 3, - "digest": "3ed5b27c172d48f83929715ba92d1030a282f9f1e29ec2fcdd3d7e9efbc54a84" + "captureGroups": 7, + "digest": "83c36832f24446fe03a07288dd394bc7494a54e5979c9b71c1dcb8b19ab54341" }, "rust-traits/src/traits/drawable.rs": { - "captureGroups": 5, - "digest": "1dca39bbc7c1b1b66f1a34730b9a5b4dba04c54ee9d2688255e0fd4e6bc48499" + "captureGroups": 9, + "digest": "cee5091f041038722f1f012394a75ba4e16870d05b2dafa37371200e785198f0" }, "rust-union/lib.rs": { "captureGroups": 10, From 67d55d7e59d552d50625b6122c630795776b3679 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 15:08:06 +0000 Subject: [PATCH 05/10] fix(storage): bump INCREMENTAL_SCHEMA_VERSION for Rust dyn-dispatch fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RUST_SCOPE_QUERY gained a function_signature_item capture (previous commit) so abstract trait methods can now dispatch a CALLS edge through a &dyn Trait receiver. The incremental write set only covers changed files, so a top-up against a pre-v11 index would keep silently missing these edges for every unchanged Rust trait file — same contract as v7/v10; force a full re-analyze instead. --- gitnexus/src/storage/repo-manager.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index e34432a7a..9852e8ba4 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -437,8 +437,15 @@ export interface RepoMeta { * `Record` node and its `HAS_METHOD` edges for every unchanged record file * (same v7 contract: new nodes/edges the incremental path would otherwise * never backfill); force a full re-analyze instead. + * v11: Rust abstract trait methods (`fn foo(&self) -> T;`, no body) now get a + * scope + declaration capture (#2604): RUST_SCOPE_QUERY had no + * `function_signature_item` pattern, so a `&dyn Trait` receiver could never + * dispatch a CALLS edge to the trait's own method. Same v7/v10 contract: the + * incremental write set only covers changed files, so a top-up against a + * pre-v11 index would keep silently missing these CALLS edges for every + * unchanged Rust trait file; force a full re-analyze instead. */ -export const INCREMENTAL_SCHEMA_VERSION = 10; +export const INCREMENTAL_SCHEMA_VERSION = 11; export interface IndexedRepo { repoPath: string; From 66f11badaf8da44af9bdd64c662611928780a390 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 15:19:48 +0000 Subject: [PATCH 06/10] style: wrap long filter predicate per prettier (PR autofix) --- gitnexus/test/integration/resolvers/rust.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index 87aa6899b..fe970b777 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -1969,7 +1969,9 @@ describe('Rust dyn trait-object dispatch (#2604)', () => { it('emits exactly one CALLS edge from calls_via_dyn(b: &dyn Behaviour) to trait_target', () => { const calls = getRelationships(result, 'CALLS'); - const dynCalls = calls.filter((c) => c.source === 'calls_via_dyn' && c.target === 'trait_target'); + const dynCalls = calls.filter( + (c) => c.source === 'calls_via_dyn' && c.target === 'trait_target', + ); expect(dynCalls.length).toBe(1); }); }); From 00141d0da2c74c6f5f6aadb69ec9a754033f1097 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 15:32:11 +0000 Subject: [PATCH 07/10] test(bench): rebaseline rust scope-capture fingerprint for #2604 RUST_SCOPE_QUERY gained a function_signature_item capture, shifting the capture fingerprint for every bench fixture with a required trait method. Verified: node --import tsx bench/scope-capture/measure.mjs --check now passes across all 14 languages (rust scaling 1.036 < 1.5 budget). --- gitnexus/bench/scope-capture/baselines.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index cab3765a4..81e8b92ec 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -46,8 +46,9 @@ "_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11)." }, "rust": { - "fingerprint": "df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29", + "fingerprint": "f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846", "scaling_budget": 1.5, + "_rebaselined_dyn_trait_object_2604": "#2604: RUST_SCOPE_QUERY now captures function_signature_item (abstract trait methods, no body) as a scope + declaration, so a &dyn Trait receiver can dispatch a CALLS edge to the trait's own method. Additive capture shift across every bench fixture with a required trait method. Prior df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29 -> f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846; scaling 1.033 < 1.5.", "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c -> df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29; scaling 1.065 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Rust fn-value callable flow facts with invocation/constructor-result suppression. Prior ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82 -> 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c; scaling 1.024 < 1.5.", "_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) \u2014 legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", From a7bfe819ebc0eb8267f00b062643dda64a324618 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 15:42:58 +0000 Subject: [PATCH 08/10] test: update hardcoded schema-version expectations for v11 (#2604) call-summary-schema-version.test.ts pins INCREMENTAL_SCHEMA_VERSION as a literal per bump, documenting the reuse-gate boundary for each version. Update the "current" expectation to 11 and add the v10 pre-current case, matching the v7/v8/v9/v10 precedent already in the file. --- gitnexus/test/unit/call-summary-schema-version.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gitnexus/test/unit/call-summary-schema-version.test.ts b/gitnexus/test/unit/call-summary-schema-version.test.ts index a8240fa0f..2047754c4 100644 --- a/gitnexus/test/unit/call-summary-schema-version.test.ts +++ b/gitnexus/test/unit/call-summary-schema-version.test.ts @@ -73,8 +73,8 @@ describe('CALL_SUMMARY relation-type exclusion (U-C1)', () => { }); describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { - it('INCREMENTAL_SCHEMA_VERSION is bumped to 10 (Java record container-node re-index window)', () => { - expect(INCREMENTAL_SCHEMA_VERSION).toBe(10); + it('INCREMENTAL_SCHEMA_VERSION is bumped to 11 (Rust dyn-trait-object dispatch re-index window, #2604)', () => { + expect(INCREMENTAL_SCHEMA_VERSION).toBe(11); }); it('a pre-current stamp fails the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate → forces full re-analyze', () => { @@ -112,7 +112,11 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { // (#2564) — a record's methods would keep being ownerless Method nodes // with no HAS_METHOD edge on unchanged files → must NOT reuse. expect(passesReuseGate(9)).toBe(false); + // A pre-v11 (v10) index predates the Rust dyn-trait-object dispatch fix + // (#2604) — abstract trait methods would keep being uncaptured (no + // ownerId/CALLS resolution) on unchanged Rust trait files → must NOT reuse. + expect(passesReuseGate(10)).toBe(false); // A current-version stamp passes the gate (incremental top-up eligible). - expect(passesReuseGate(10)).toBe(true); + expect(passesReuseGate(11)).toBe(true); }); }); From e18b4416c6cc7cafc48b66cc1acb31368598ea68 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 16:10:01 +0000 Subject: [PATCH 09/10] test(rust): cover Box and dyn-bound-list normalization (#2604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitNexus review-agent finding: stripDynBound's documented Box, Rc/Arc, and auto-trait/lifetime bound-list (dyn Trait + Send) shapes had no test anywhere — only the bare &dyn Trait parameter case was exercised end-to-end. Add direct unit coverage on normalizeRustTypeName and (via interpretRustTypeBinding) normalizeRustReturnType for these shapes. --- .../rust/rust-dyn-type-normalization.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts diff --git a/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts b/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts new file mode 100644 index 000000000..3a338830e --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import type { CaptureMatch } from 'gitnexus-shared'; +import { + normalizeRustTypeName, + interpretRustTypeBinding, +} from '../../../../src/core/ingestion/languages/rust/interpret.js'; + +const RANGE = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 }; + +/** Builds a minimal @type-binding.return CaptureMatch to exercise + * normalizeRustReturnType (private, only reachable through this hook). */ +function returnTypeBinding(type: string): CaptureMatch { + return { + '@type-binding.name': { name: '@type-binding.name', range: RANGE, text: 'f' }, + '@type-binding.type': { name: '@type-binding.type', range: RANGE, text: type }, + '@type-binding.return': { name: '@type-binding.return', range: RANGE, text: '' }, + }; +} + +/** + * #2604 coverage gap (GitNexus review-agent finding): stripDynBound's + * documented Box and bound-list (dyn Trait + Send) shapes had no + * test anywhere, even though the interpret.ts comment claims they're handled. + * These exercise normalizeRustTypeName/normalizeRustReturnType directly — + * stripDynBound itself is a private helper reached only through them. + */ +describe('Rust dyn-trait-object type-name normalization (#2604)', () => { + it('strips a bare dyn Trait parameter type', () => { + expect(normalizeRustTypeName('&dyn Behaviour')).toBe('Behaviour'); + expect(normalizeRustTypeName('dyn Behaviour')).toBe('Behaviour'); + }); + + it('strips dyn through Box/Rc/Arc wrappers', () => { + expect(normalizeRustTypeName('Box')).toBe('Trait'); + expect(normalizeRustTypeName('Rc')).toBe('Trait'); + expect(normalizeRustTypeName('Arc')).toBe('Trait'); + }); + + it('drops an auto-trait/lifetime bound list after dyn', () => { + expect(normalizeRustTypeName('dyn Trait + Send')).toBe('Trait'); + expect(normalizeRustTypeName("dyn Trait + Send + 'static")).toBe('Trait'); + expect(normalizeRustTypeName("Box")).toBe('Trait'); + }); + + it('truncates a dyn trait\'s own generic arguments after stripping dyn', () => { + expect(normalizeRustTypeName('dyn Iterator')).toBe('Iterator'); + }); + + it('strips dyn in return-type position, including through &', () => { + expect(interpretRustTypeBinding(returnTypeBinding('&dyn Trait'))?.rawTypeName).toBe('Trait'); + expect(interpretRustTypeBinding(returnTypeBinding('dyn Trait + Send'))?.rawTypeName).toBe( + 'Trait', + ); + }); + + it('leaves ordinary (non-dyn) type names untouched', () => { + expect(normalizeRustTypeName('Behaviour')).toBe('Behaviour'); + expect(normalizeRustTypeName('&Behaviour')).toBe('Behaviour'); + expect(normalizeRustTypeName('Box')).toBe('Behaviour'); + }); +}); From 9f57984372f6e71637ef9c3583125fdddb574400 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Jul 2026 16:12:20 +0000 Subject: [PATCH 10/10] style: fix quote style per prettier in new dyn-normalization test --- .../scope-resolution/rust/rust-dyn-type-normalization.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts b/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts index 3a338830e..273741764 100644 --- a/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts +++ b/gitnexus/test/unit/scope-resolution/rust/rust-dyn-type-normalization.test.ts @@ -42,7 +42,7 @@ describe('Rust dyn-trait-object type-name normalization (#2604)', () => { expect(normalizeRustTypeName("Box")).toBe('Trait'); }); - it('truncates a dyn trait\'s own generic arguments after stripping dyn', () => { + it("truncates a dyn trait's own generic arguments after stripping dyn", () => { expect(normalizeRustTypeName('dyn Iterator')).toBe('Iterator'); });