From 6f0f8b1deeb59377f6c90b5257297cf67f5f52ef Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Tue, 18 Aug 2026 16:59:32 +0200 Subject: [PATCH] fix(zig): keyword-gate the constructor type-binding rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../src/core/ingestion/languages/zig/query.ts | 25 +++++++++++++--- gitnexus/test/unit/zig-extractors.test.ts | 30 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/zig/query.ts b/gitnexus/src/core/ingestion/languages/zig/query.ts index de8c46dbc..778d7a4ed 100644 --- a/gitnexus/src/core/ingestion/languages/zig/query.ts +++ b/gitnexus/src/core/ingestion/languages/zig/query.ts @@ -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 diff --git a/gitnexus/test/unit/zig-extractors.test.ts b/gitnexus/test/unit/zig-extractors.test.ts index b9c501eff..cb139e976 100644 --- a/gitnexus/test/unit/zig-extractors.test.ts +++ b/gitnexus/test/unit/zig-extractors.test.ts @@ -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([ {