Merge branch 'main' into fix/2605-rename-edit-count

This commit is contained in:
Gergő Magyar 2026-07-21 17:34:36 +01:00 committed by GitHub
commit aa8a441202
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 162 additions and 19 deletions

View file

@ -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.",

View file

@ -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<dyn Trait>` 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<T> 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('::');

View file

@ -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

View file

@ -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;

View file

@ -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()
}

View file

@ -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,

View file

@ -1951,6 +1951,31 @@ 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
//

View file

@ -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);
});
});

View file

@ -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<dyn Trait> 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<dyn Trait>')).toBe('Trait');
expect(normalizeRustTypeName('Rc<dyn Trait>')).toBe('Trait');
expect(normalizeRustTypeName('Arc<dyn Trait>')).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<dyn Trait + 'static>")).toBe('Trait');
});
it("truncates a dyn trait's own generic arguments after stripping dyn", () => {
expect(normalizeRustTypeName('dyn Iterator<Item = u32>')).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<Behaviour>')).toBe('Behaviour');
});
});