diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts b/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts index 2665f6a0d..6cd044c37 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts @@ -52,13 +52,17 @@ const extractZigReturnType = (node: SyntaxNode): string | undefined => { /** * Regular parameters only. The receiver parameter (`zigReceiverParameter`) is * reported through `extractReceiverType`, not the parameter list (same split - * as Rust's `self_parameter` skip in `configs/rust.ts`). + * as Rust's `self_parameter` skip in `configs/rust.ts`). `filePath` is what + * names a file-struct (`fn add(ledger: *Ledger)` in `Ledger.zig`): without it + * the receiver rule cannot see the file stem and such a fn reads as static + * with the receiver in its arity — an id the scope side, which always has + * the path, never produces, so its CALLS edges went nowhere. */ -const extractZigParameters = (node: SyntaxNode): ParameterInfo[] => { +const extractZigParameters = (node: SyntaxNode, filePath?: string): ParameterInfo[] => { const paramList = zigParameterList(node); if (!paramList) return []; const params: ParameterInfo[] = []; - const receiver = zigReceiverParameter(node); + const receiver = zigReceiverParameter(node, filePath); for (let i = 0; i < paramList.namedChildCount; i++) { const param = paramList.namedChild(i); if (!param || param.type !== 'parameter') continue; @@ -76,8 +80,8 @@ const extractZigParameters = (node: SyntaxNode): ParameterInfo[] => { return params; }; -const extractZigReceiverType = (node: SyntaxNode): string | undefined => - zigReceiverParameter(node)?.childForFieldName('type')?.text?.trim(); +const extractZigReceiverType = (node: SyntaxNode, filePath?: string): string | undefined => + zigReceiverParameter(node, filePath)?.childForFieldName('type')?.text?.trim(); /** * Names a `test_declaration` during the enclosing-function walk (parse-worker @@ -119,11 +123,12 @@ export const zigMethodConfig: MethodExtractionConfig = { extractVisibility: (node) => (hasZigPubKeyword(node) ? 'public' : 'private'), extractReceiverType: extractZigReceiverType, - isStatic(node) { + isStatic(node, filePath) { // A Zig "method" is static when it has no receiver parameter — `self` OR // a first parameter typed as the enclosing container (`replica: - // *Replica`, `pool: *@This()`); see `zigReceiverParameter`. - return zigReceiverParameter(node) === null; + // *Replica`, `pool: *@This()`, `ledger: *Ledger` in `Ledger.zig`); see + // `zigReceiverParameter`. + return zigReceiverParameter(node, filePath) === null; }, isAbstract() { diff --git a/gitnexus/src/core/ingestion/method-extractors/generic.ts b/gitnexus/src/core/ingestion/method-extractors/generic.ts index b47272525..3b92f4531 100644 --- a/gitnexus/src/core/ingestion/method-extractors/generic.ts +++ b/gitnexus/src/core/ingestion/method-extractors/generic.ts @@ -247,13 +247,15 @@ function buildMethod( // Static-owner detection is config-driven: each language declares which // container node types imply static (e.g. Ruby singleton_class, Kotlin companion_object). - const isStatic = (config.staticOwnerTypes?.has(ownerNode.type) ?? false) || config.isStatic(node); + const isStatic = + (config.staticOwnerTypes?.has(ownerNode.type) ?? false) || + config.isStatic(node, context.filePath); return { name, - receiverType: config.extractReceiverType?.(node) ?? null, + receiverType: config.extractReceiverType?.(node, context.filePath) ?? null, returnType: config.extractReturnType(node) ?? null, - parameters: config.extractParameters(node), + parameters: config.extractParameters(node, context.filePath), visibility: config.extractVisibility(node), isStatic, isAbstract, diff --git a/gitnexus/src/core/ingestion/method-types.ts b/gitnexus/src/core/ingestion/method-types.ts index 67eec253c..5e8f833b5 100644 --- a/gitnexus/src/core/ingestion/method-types.ts +++ b/gitnexus/src/core/ingestion/method-types.ts @@ -89,13 +89,19 @@ export interface MethodExtractionConfig { bodyNodeTypes: string[]; extractName: (node: SyntaxNode) => string | undefined; extractReturnType: (node: SyntaxNode) => string | undefined; - extractParameters: (node: SyntaxNode) => ParameterInfo[]; + /** The optional `filePath` (the extractor context's) is passed to + * `extractParameters`, `isStatic` and `extractReceiverType` for languages + * whose receiver rule depends on the file — Zig's file-struct, whose type + * name is the file stem, so `fn incr(counter: *Counter)` in `Counter.zig` + * is a method only when the file is known. Same optional-trailing-argument + * shape as `extractOwnerName`; every other config ignores it. */ + extractParameters: (node: SyntaxNode, filePath?: string) => ParameterInfo[]; extractVisibility: (node: SyntaxNode) => MethodVisibility; - isStatic: (node: SyntaxNode) => boolean; + isStatic: (node: SyntaxNode, filePath?: string) => boolean; isAbstract: (node: SyntaxNode, ownerNode: SyntaxNode) => boolean; isFinal: (node: SyntaxNode) => boolean; extractAnnotations?: (node: SyntaxNode) => string[]; - extractReceiverType?: (node: SyntaxNode) => string | undefined; + extractReceiverType?: (node: SyntaxNode, filePath?: string) => string | undefined; isVirtual?: (node: SyntaxNode) => boolean; isOverride?: (node: SyntaxNode) => boolean; isAsync?: (node: SyntaxNode) => boolean; diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index df7f9b948..fe7f4506b 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -312,8 +312,8 @@ export const LINKABLE_LABELS: ReadonlySet = new Set([ // // Covers every language that spells an alias this way — TypeScript, Kotlin, // Dart and Rust all emit `@declaration.type_alias`. The remaining - // `CLASS_KINDS` entries (Typedef, Delegate, Annotation, Template) plausibly - // have the same gap, but nothing exercises them today + // `CLASS_KINDS` entries (Typedef, Delegate, Annotation, Template, Namespace) + // plausibly have the same gap, but nothing exercises them today // and adding labels no test covers is how this list drifts out of sync with // what it claims. 'TypeAlias', diff --git a/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/Ledger.zig b/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/Ledger.zig new file mode 100644 index 000000000..10d59ee4a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/Ledger.zig @@ -0,0 +1,12 @@ +// A file-struct with NO `const Self = @This();` alias: the only spelling of +// its type is the file stem, so the receiver rule needs the file path. +total: u64 = 0, +pub fn add(ledger: *Ledger, n: u64) void { + ledger.total += n; +} +pub fn sum(ledger: Ledger) u64 { + return ledger.total; +} +pub fn empty() Ledger { + return .{}; +} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/main.zig b/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/main.zig index 63c71133a..31abd0460 100644 --- a/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/main.zig +++ b/gitnexus/test/fixtures/lang-resolution/zig-receivers/src/main.zig @@ -1,6 +1,7 @@ const stdx = @import("stdx/stdx.zig"); const counter = @import("counter.zig"); const Counter = counter.Counter; +const Ledger = @import("Ledger.zig"); fn use_named_receiver() void { var c = Counter{}; @@ -25,6 +26,12 @@ fn use_hub_static_call() u64 { return prng.next(); } +fn use_file_struct_receiver() u64 { + var ledger = Ledger.empty(); + ledger.add(3); + return ledger.sum(); +} + fn use_hub_generic_annotation() usize { var headers: stdx.BoundedArrayType(u8, 4) = .{}; return headers.count(); @@ -36,4 +43,5 @@ pub fn main() void { _ = use_enum_variant_receiver(); _ = use_hub_static_call(); _ = use_hub_generic_annotation(); + _ = use_file_struct_receiver(); } diff --git a/gitnexus/test/integration/resolvers/zig.test.ts b/gitnexus/test/integration/resolvers/zig.test.ts index a09c64ace..23d51aff6 100644 --- a/gitnexus/test/integration/resolvers/zig.test.ts +++ b/gitnexus/test/integration/resolvers/zig.test.ts @@ -963,6 +963,30 @@ describe.skipIf(!zigAvailable)( expect(methods.has('Method:src/counter.zig:Pool.release#2')).toBe(false); }); + it('labels a file-struct receiver typed by the file stem (`ledger: *Ledger` in `Ledger.zig`)', () => { + // `Ledger.zig` declares no `Self` alias: the stem is the only spelling of + // its type, and only the file path can supply it. The structure phase + // used to build these methods without the path, so `add` came out static + // as `Ledger.add#2` while the scope side (which has the path) resolved + // `ledger.add(3)` to `Ledger.add#1` — an id that did not exist. + const methods = new Map(); + result.graph.forEachNode((n) => { + if (n.label === 'Method') methods.set(n.id, n.properties.isStatic === true); + }); + expect(methods.get('Method:src/Ledger.zig:Ledger.add#1')).toBe(false); + expect(methods.get('Method:src/Ledger.zig:Ledger.sum#0')).toBe(false); + expect(methods.get('Method:src/Ledger.zig:Ledger.empty#0')).toBe(true); + expect(methods.has('Method:src/Ledger.zig:Ledger.add#2')).toBe(false); + const calls = edgeSet(getRelationships(result, 'CALLS')); + expect(calls).toEqual( + expect.arrayContaining([ + 'use_file_struct_receiver → empty', + 'use_file_struct_receiver → add', + 'use_file_struct_receiver → sum', + ]), + ); + }); + it('dispatches calls onto those methods exactly as onto `self` methods', () => { const calls = edgeSet(getRelationships(result, 'CALLS')); expect(calls).toEqual( diff --git a/gitnexus/test/unit/zig-extractors.test.ts b/gitnexus/test/unit/zig-extractors.test.ts index e32b387f2..1378b202d 100644 --- a/gitnexus/test/unit/zig-extractors.test.ts +++ b/gitnexus/test/unit/zig-extractors.test.ts @@ -249,6 +249,43 @@ pub fn Pool(comptime Node: type) type { expect(poolByName.get('acquire')!.receiverType).toBe('*Pool(Node)'); expect(poolByName.get('acquire')!.isStatic).toBe(false); }); + + it('reads a file-struct receiver typed by the file stem — the rule needs the file path', () => { + // `Ledger.zig` with top-level fields IS the type `Ledger`; without a + // `const Self = @This();` alias the stem is the only spelling. The method + // builder must hand the config its `filePath`: without it `zigReceiverParameter` + // cannot name the file-struct, so `add` read as static with `ledger` in + // its arity (`Ledger.add#2`) — an id the scope side never produces, so + // every call to it was dropped. + const root = parse(` +total: u64 = 0, +pub fn add(ledger: *Ledger, n: u64) void { ledger.total += n; } +pub fn sum(ledger: Ledger) u64 { return ledger.total; } +pub fn empty() Ledger { return .{}; } +`).rootNode; + const file = extractor.extract(root, { + filePath: 'src/Ledger.zig', + language: SupportedLanguages.Zig, + })!; + const byName = new Map(file.methods.map((m) => [m.name, m])); + expect(byName.get('add')!.receiverType).toBe('*Ledger'); + expect(byName.get('add')!.isStatic).toBe(false); + expect(byName.get('add')!.parameters.map((p) => p.name)).toEqual(['n']); + expect(byName.get('sum')!.receiverType).toBe('Ledger'); + expect(byName.get('sum')!.isStatic).toBe(false); + expect(byName.get('empty')!.isStatic).toBe(true); + // Under another file name the same source is a namespace: `Ledger` is + // then some other type, and `add` is a plain static fn of two parameters. + const other = extractor.extract(root, { + filePath: 'src/Book.zig', + language: SupportedLanguages.Zig, + })!; + expect(other.methods.find((m) => m.name === 'add')!.isStatic).toBe(true); + expect(other.methods.find((m) => m.name === 'add')!.parameters.map((p) => p.name)).toEqual([ + 'ledger', + 'n', + ]); + }); }); describeZig('Zig VariableExtractor — container and import bindings are not variables', () => {