fix(zig): keyword-gate the constructor type-binding rules

The three `@type-binding.constructor` rules (`const p = T{…}`, `mod.T{…}`,
`List(u8){…}`) matched any `variable_declaration` with an identifier and a
`struct_initializer`, keyword or not — and tree-sitter-zig 1.1.2 parses a
re-assignment `p = T{…};` (and `_ = T{…};`) as the same node type. So an
assignment minted a constructor binding for `p` in its own block, and one
for `_`. Zig's static typing makes the extra binding redundant (`p`
already carries its type from its declaration: annotation, constructor or
inferred value), so it cost little, but it declared nothing and stood out
against every other binding rule (`@declaration.variable`, the import
rules, the call-return rules), which are keyword-gated for exactly this
shape. Split each rule into `"const" .` / `"var" .` variants, like the
call-return rules.

Regression test in `zig-extractors.test.ts`: `p = T{…}`, `q = mod.T{…}`,
`l = List(u8){}` and `_ = T{…}` after their declarations yield only the
three declaration bindings (fails on the previous query). The zig,
callable-value-flow, grammar-literal and tree-sitter-languages suites are
unchanged.

Raised twice by gitnexus-check (passes on 2026-08-18 12:42 and 12:56).
This commit is contained in:
Navid EMAD 2026-08-18 16:59:32 +02:00
parent fdfdd2e35f
commit 6f0f8b1dee
No known key found for this signature in database
2 changed files with 51 additions and 4 deletions

View file

@ -225,9 +225,18 @@ const ZIG_SCOPE_QUERY = `
name: (identifier) @type-binding.name
type: (_) @type-binding.type) @type-binding.parameter
;; Type bindings — constructor inference: const p = T{ ... }
;; Type bindings — constructor inference: const p = T{ ... }. Keyword-gated
;; like every binding rule: a keyword-less \`p = T{ ... };\` is a
;; re-assignment (same node type in tree-sitter-zig 1.1.2), and Zig's static
;; typing means \`p\` already carries its type from its declaration
;; (annotation, constructor or inferred value) — the assignment declares
;; nothing, and \`_ = T{ ... };\` must not bind \`_\`.
(variable_declaration
(identifier) @type-binding.name
"const" . (identifier) @type-binding.name
(struct_initializer
(identifier) @type-binding.type)) @type-binding.constructor
(variable_declaration
"var" . (identifier) @type-binding.name
(struct_initializer
(identifier) @type-binding.type)) @type-binding.constructor
@ -236,7 +245,11 @@ const ZIG_SCOPE_QUERY = `
;; receiver dispatch resolves the namespace prefix through the import
;; binding (emitReceiverBoundCalls Case 3).
(variable_declaration
(identifier) @type-binding.name
"const" . (identifier) @type-binding.name
(struct_initializer
(field_expression) @type-binding.type)) @type-binding.constructor
(variable_declaration
"var" . (identifier) @type-binding.name
(struct_initializer
(field_expression) @type-binding.type)) @type-binding.constructor
@ -244,7 +257,11 @@ const ZIG_SCOPE_QUERY = `
;; The callee is the type constructor; \`normalizeZigTypeName\` drops the
;; comptime argument list so \`List(u8)\` looks up \`List\`.
(variable_declaration
(identifier) @type-binding.name
"const" . (identifier) @type-binding.name
(struct_initializer
(call_expression) @type-binding.type)) @type-binding.constructor
(variable_declaration
"var" . (identifier) @type-binding.name
(struct_initializer
(call_expression) @type-binding.type)) @type-binding.constructor

View file

@ -1390,6 +1390,36 @@ fn f() !void {
]);
});
it('a keyword-less re-assignment `p = T{…};` / `_ = T{…};` binds no constructor type (only declarations do)', () => {
// tree-sitter-zig 1.1.2 parses assignments as `variable_declaration` too;
// the constructor rules used to match them and mint a binding for `p` (and
// for `_`) in the assignment's block. `p` already carries its type from its
// declaration, so the assignment adds nothing — the rules are keyword-gated
// like the import and call-return rules.
const src = `
fn f() void {
var p = Thing{ .a = 1 };
const q = mod.Thing{ .a = 1 };
const l = List(u8){};
p = Thing{ .a = 2 };
q = mod.Thing{ .a = 2 };
l = List(u8){};
_ = Thing{ .a = 3 };
}
`;
const ctor = (boundName: string, rawTypeName: string) => ({
boundName,
rawTypeName,
source: 'constructor-inferred',
kind: '@type-binding.constructor',
});
expect(bindingsOf(src)).toEqual([
ctor('p', 'Thing'),
ctor('q', 'mod.Thing'),
ctor('l', 'List'),
]);
});
it('a wrapped struct literal (`try Thing{…}`) is a constructor binding', () => {
expect(bindingsOf('fn f() !void { const t = try Thing{ .a = 1 }; }')).toEqual([
{