From bb3642ad5f8234359a4a129d8a46203dfde78dba Mon Sep 17 00:00:00 2001 From: Sparsh <73558748+prajapatisparsh@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:16:07 +0530 Subject: [PATCH] =?UTF-8?q?fix(rust):=20F70=20=E2=80=94=20replace=20struct?= =?UTF-8?q?=5Fexpression=20name:(=5F)=20with=20three=20specific=20patterns?= =?UTF-8?q?=20(#2051)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(rust): F70 — replace struct_expression name:(_) with 3 specific patterns * fix(rust): F70 — cover scoped+turbofish struct literals (foo::Bar:: {}) The three patterns enumerate struct_expression.name as type_identifier / scoped_type_identifier / generic_type_with_turbofish, but generic_type_with_turbofish.type can itself be a scoped_identifier (e.g. foo::Bar:: {}), which the turbofish pattern — requiring type:(type_identifier) — did not match. That dropped the constructor reference entirely (verified: emitRustScopeCaptures returns 0 ctors for foo::Bar:: {} and a::b::Bar:: {}). Add a fourth pattern that captures the trailing identifier of the scoped turbofish path (scoped_identifier.name is an identifier, not a type_identifier), and correct the comment that claimed all cases were covered. Strengthen rust-f70.test.ts: assert exactly one constructor per case, add negative assertions guarding against the old full-path capture, and add the scoped+turbofish and crate:: cases. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Gergo Magyar Co-authored-by: Claude Opus 4.8 (1M context) --- .../core/ingestion/languages/rust/query.ts | 31 ++++++++-- .../integration/resolvers/rust-f70.test.ts | 56 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 gitnexus/test/integration/resolvers/rust-f70.test.ts diff --git a/gitnexus/src/core/ingestion/languages/rust/query.ts b/gitnexus/src/core/ingestion/languages/rust/query.ts index d85660981..a0e75f0fa 100644 --- a/gitnexus/src/core/ingestion/languages/rust/query.ts +++ b/gitnexus/src/core/ingestion/languages/rust/query.ts @@ -134,11 +134,34 @@ const RUST_SCOPE_QUERY = ` name: (identifier) @reference.name)) @reference.call.free ;; References — constructor calls (struct literal) -;; Covers bare names (Foo {}), scoped (foo::bar::Baz {}), and turbofish -;; (Foo:: {}) — the name: field resolves to the trailing identifier -;; in all cases through tree-sitter-rust's grammar. +;; tree-sitter-rust gives struct_expression.name one of three node types +;; (type_identifier | scoped_type_identifier | generic_type_with_turbofish); +;; the turbofish form additionally nests either a type_identifier or a +;; scoped_identifier. We enumerate all four shapes below so the capture is +;; always the trailing identifier (resolved scope-aware), not the full path: +;; bare Foo {} +;; scoped foo::bar::Baz {} +;; turbofish Foo:: {} +;; scoped+turbofish foo::Bar:: {} (struct_expression - name: (_) @reference.name) @reference.call.constructor + name: (type_identifier) @reference.name) @reference.call.constructor + +;; Scoped struct (foo::bar::Baz {}) +(struct_expression + name: (scoped_type_identifier + name: (type_identifier) @reference.name)) @reference.call.constructor + +;; Turbofish struct (Foo:: {}) +(struct_expression + name: (generic_type_with_turbofish + type: (type_identifier) @reference.name)) @reference.call.constructor + +;; Scoped + turbofish struct (foo::Bar:: {}) — the turbofish wraps a +;; scoped_identifier whose tail is an identifier (not a type_identifier). +(struct_expression + name: (generic_type_with_turbofish + type: (scoped_identifier + name: (identifier) @reference.name))) @reference.call.constructor ;; References — macro invocations (disjoint namespace from functions) ;; Resolved via MacroRegistry → Macro defs only (never fn of the same name). diff --git a/gitnexus/test/integration/resolvers/rust-f70.test.ts b/gitnexus/test/integration/resolvers/rust-f70.test.ts new file mode 100644 index 000000000..d536ee917 --- /dev/null +++ b/gitnexus/test/integration/resolvers/rust-f70.test.ts @@ -0,0 +1,56 @@ +/** + * Tests for F70 — struct literal constructor calls (issue #1934). + */ +import { describe, it, expect } from 'vitest'; +import { emitRustScopeCaptures } from '../../../src/core/ingestion/languages/rust/index.js'; +import type { CaptureMatch } from 'gitnexus-shared'; + +describe('F70 — struct literal constructor calls', () => { + it('bare struct Foo {} captures Foo as @reference.name', () => { + const src = `fn f() { let _ = Foo { x: 1 }; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const ctors = matches.filter((m) => m['@reference.call.constructor']); + expect(ctors.length).toBe(1); + expect(ctors[0]['@reference.name'].text).toBe('Foo'); + }); + + it('scoped struct foo::bar::Baz {} captures Baz (not the full path)', () => { + const src = `fn f() { let _ = foo::bar::Baz { x: 1 }; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const ctors = matches.filter((m) => m['@reference.call.constructor']); + const names = ctors.map((m) => m['@reference.name']?.text); + expect(ctors.length).toBe(1); + expect(names).toContain('Baz'); + // Guard against regressing to the old wildcard, which captured the path. + expect(names).not.toContain('foo::bar::Baz'); + }); + + it('turbofish struct Foo:: {} captures Foo (not Foo::)', () => { + const src = `fn f() { let _ = Foo:: { x: 1 }; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const ctors = matches.filter((m) => m['@reference.call.constructor']); + const names = ctors.map((m) => m['@reference.name']?.text); + expect(ctors.length).toBe(1); + expect(names).toContain('Foo'); + expect(names).not.toContain('Foo::'); + }); + + it('scoped + turbofish struct foo::Bar:: {} captures Bar', () => { + const src = `fn f() { let _ = foo::Bar:: { x: 1 }; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const ctors = matches.filter((m) => m['@reference.call.constructor']); + const names = ctors.map((m) => m['@reference.name']?.text); + expect(ctors.length).toBe(1); + expect(names).toContain('Bar'); + expect(names).not.toContain('foo::Bar::'); + }); + + it('crate-scoped struct crate::Foo {} captures Foo', () => { + const src = `fn f() { let _ = crate::Foo { x: 1 }; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const ctors = matches.filter((m) => m['@reference.call.constructor']); + const names = ctors.map((m) => m['@reference.name']?.text); + expect(ctors.length).toBe(1); + expect(names).toContain('Foo'); + }); +});