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'); + }); +});