fix(rust): F70 — replace struct_expression name:(_) with three specific patterns (#2051)

* fix(rust): F70 — replace struct_expression name:(_) with 3 specific patterns

* fix(rust): F70 — cover scoped+turbofish struct literals (foo::Bar::<T> {})

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::<i32> {}), 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::<i32> {} and a:🅱️:Bar::<i32> {}).

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) <noreply@anthropic.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sparsh 2026-06-05 22:16:07 +05:30 • committed by GitHub
parent 782f70cc07
commit bb3642ad5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 4 deletions

View file

@ -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::<T> {}) — 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::<T> {}
;; scoped+turbofish foo::Bar::<T> {}
(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::<T> {})
(struct_expression
name: (generic_type_with_turbofish
type: (type_identifier) @reference.name)) @reference.call.constructor
;; Scoped + turbofish struct (foo::Bar::<T> {}) — 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).

View file

@ -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::<i32> {} captures Foo (not Foo::<i32>)', () => {
const src = `fn f() { let _ = Foo::<i32> { 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::<i32>');
});
it('scoped + turbofish struct foo::Bar::<i32> {} captures Bar', () => {
const src = `fn f() { let _ = foo::Bar::<i32> { 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::<i32>');
});
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');
});
});