mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
feat(zig): track qualified struct literals (mod.T{…}) as construction sites
PR #1432 re-test (issue comment on 97571d23): 163 qualified literals
`mod.Type{ … }` in a real project produced no CALLS edge at all, so only
same-file and imported-name literals were tracked as construction sites.
One query rule captures `(struct_initializer (field_expression object
member))` as `@reference.call.constructor` WITH the receiver. Captured as a
free constructor instead, the site resolves by its simple tail and a
workspace-unique `Thing` answers for `c.Thing{}` whichever module the
source named (measured: c.zig defines no `Thing`, the edge went to a.zig's).
With the receiver the site takes the receiver-bound namespace case — the
path `mod.fn()` takes — which resolves inside the module the receiver is
bound to: `a.Thing{}` / `b.Thing{}` bind their own files, `c.Thing{}` binds
nothing, `std.Thread.Mutex{}` binds nothing next to a local `Mutex`.
That case's edge now goes through `constructionSiteReason` too, so the
opt-in marker (`import-resolved (constructor)` / `global (constructor)`)
reaches it; `markConstructionSites` joins `ReceiverBoundProviderSubset`.
Byte-identical for every provider that does not set the flag.
Also answers the twelfth gitnexus-check pass: the `bodyNodeSet.size === 0`
guard on the extractor factories' no-wrapper branch is deliberate (a config
with wrappers whose node lacks one is a bodiless declaration); the two
comments now say so instead of reading as a universal last resort. Go's
method config, the only other empty-`bodyNodeTypes` config, never reaches
the branch (its `extract()` gates on method/function nodes the class-node
caller never passes).
Tests: new `zig-qualified-literal` fixture (same-named `Thing` in two
modules, a module without it, an external `std` qualifier next to a local
and an imported `Mutex`); `zig-basic` pins `pioneer.Pioneer{…}` and the
union `pioneer.Tag{…}` as marked construction sites.
This commit is contained in:
parent
97571d23f5
commit
e2b13233ce
10 changed files with 170 additions and 11 deletions
|
|
@ -149,10 +149,14 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
|
|||
if (result.length === 0 && bodyField) {
|
||||
result.push(bodyField);
|
||||
}
|
||||
// Last resort: when no body wrapper exists (e.g. tree-sitter-zig's
|
||||
// struct_declaration directly contains its container_field children),
|
||||
// use the type-declaration node itself as the body. The downstream
|
||||
// walk filters by `fieldNodeTypes`, so unrelated children are ignored.
|
||||
// Grammars with no body wrapper at all: a config that declares NO
|
||||
// `bodyNodeTypes` (tree-sitter-zig's struct_declaration holds its
|
||||
// container_field children directly) uses the type-declaration node
|
||||
// itself as the body. The downstream walk filters by `fieldNodeTypes`,
|
||||
// so unrelated children are ignored. Deliberately NOT a fallback for
|
||||
// configs that do declare body wrappers: for them a node without its
|
||||
// wrapper is a bodiless declaration, and scanning it would change every
|
||||
// such language for no field it could find.
|
||||
if (result.length === 0 && bodyNodeSet.size === 0) {
|
||||
result.push(node);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -356,6 +356,21 @@ const ZIG_SCOPE_QUERY = `
|
|||
;; References — constructor uses: T{ ... }
|
||||
(struct_initializer
|
||||
(identifier) @reference.name) @reference.call.constructor
|
||||
|
||||
;; References — qualified constructor uses: mod.T{ ... } / hub.sub.T{ ... }
|
||||
;; Captured with the RECEIVER, not as a free constructor with a raw qualified
|
||||
;; name, on purpose: the free-call fallback resolves a qualified constructor by
|
||||
;; its simple tail, and a workspace-unique \`Thing\` then answers for
|
||||
;; \`other.Thing{}\` whichever module the source named (measured: \`c.Thing{}\`
|
||||
;; with no \`Thing\` in c.zig bound to a.zig's). With the receiver the site goes
|
||||
;; through the receiver-bound namespace case, which resolves the member inside
|
||||
;; the module the receiver is bound to — the same path \`mod.fn()\` takes — so
|
||||
;; \`a.Thing{}\` and \`b.Thing{}\` each bind their own file and \`std.Thread.Mutex{}\`
|
||||
;; binds nothing even when a local \`Mutex\` exists.
|
||||
(struct_initializer
|
||||
(field_expression
|
||||
object: (_) @reference.receiver
|
||||
member: (identifier) @reference.name)) @reference.call.constructor
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
|
|
|
|||
|
|
@ -166,10 +166,14 @@ function findBodies(node: SyntaxNode, bodyNodeSet: Set<string>): SyntaxNode[] {
|
|||
result.push(bodyField);
|
||||
addNestedBodies(bodyField, bodyNodeSet, result);
|
||||
}
|
||||
// Last resort: when no body wrapper exists (e.g. tree-sitter-zig's
|
||||
// struct_declaration directly contains its function_declaration children),
|
||||
// use the type-declaration node itself as the body. The downstream walk
|
||||
// filters by `methodNodeTypes`, so unrelated children are ignored.
|
||||
// Grammars with no body wrapper at all: a config that declares NO
|
||||
// `bodyNodeTypes` (tree-sitter-zig's struct_declaration holds its
|
||||
// function_declaration children directly) uses the type-declaration node
|
||||
// itself as the body. The downstream walk filters by `methodNodeTypes`, so
|
||||
// unrelated children are ignored. Deliberately NOT a fallback for configs
|
||||
// that do declare body wrappers: for them a node without its wrapper is a
|
||||
// bodiless declaration (forward declaration, `declare class`), and scanning
|
||||
// it would change every such language for no method it could find.
|
||||
if (result.length === 0 && bodyNodeSet.size === 0) {
|
||||
result.push(node);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import {
|
|||
tryEmitEdgeWithExplicitTargetId,
|
||||
type CalleeIdCaptureCtx,
|
||||
} from '../graph-bridge/edges.js';
|
||||
import { constructionSiteReason } from './free-call-fallback.js';
|
||||
import type { CalleeIdSink } from '../graph-bridge/callee-id-sink.js';
|
||||
import {
|
||||
resolveCompoundReceiverClass,
|
||||
|
|
@ -135,6 +136,7 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'constraintCompatibility'
|
||||
| 'isStaticOnly'
|
||||
| 'normalizeTypeArgument'
|
||||
| 'markConstructionSites'
|
||||
>;
|
||||
|
||||
/** A bare, undecorated identifier and nothing else — see {@link isBareTypeName}. */
|
||||
|
|
@ -1293,7 +1295,15 @@ export function emitReceiverBoundCalls(
|
|||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
// A namespace-qualified construction site (`mod.T{…}`) resolves
|
||||
// here like `mod.fn()` does; the provider's opt-in marker keeps
|
||||
// it distinguishable from an invocation (see
|
||||
// `ScopeResolver.markConstructionSites`).
|
||||
constructionSiteReason(
|
||||
memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
site,
|
||||
provider.markConstructionSites,
|
||||
),
|
||||
seen,
|
||||
0.85,
|
||||
collapse,
|
||||
|
|
|
|||
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/a.zig
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/a.zig
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub const Thing = struct {
|
||||
a: u32 = 0,
|
||||
};
|
||||
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/b.zig
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/b.zig
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub const Thing = struct {
|
||||
b: u32 = 0,
|
||||
};
|
||||
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/c.zig
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/c.zig
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub const Other = struct {
|
||||
c: u32 = 0,
|
||||
};
|
||||
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/d.zig
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/d.zig
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub const Mutex = struct {
|
||||
locked: bool = false,
|
||||
};
|
||||
43
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/main.zig
vendored
Normal file
43
gitnexus/test/fixtures/lang-resolution/zig-qualified-literal/src/main.zig
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const std = @import("std");
|
||||
const a = @import("a.zig");
|
||||
const b = @import("b.zig");
|
||||
const c = @import("c.zig");
|
||||
const d = @import("d.zig");
|
||||
|
||||
// A same-named local type, so an external literal below has a tempting
|
||||
// same-tail candidate in THIS file as well as in d.zig.
|
||||
const Mutex = struct {
|
||||
held: bool = false,
|
||||
};
|
||||
|
||||
fn useA() a.Thing {
|
||||
return a.Thing{ .a = 1 };
|
||||
}
|
||||
|
||||
fn useB() b.Thing {
|
||||
return b.Thing{ .b = 2 };
|
||||
}
|
||||
|
||||
fn useMissing() void {
|
||||
// `c.zig` defines no `Thing`: this must not bind to a.zig's or b.zig's.
|
||||
_ = c.Thing{};
|
||||
}
|
||||
|
||||
fn useExternal() void {
|
||||
// `std` is external: neither the local `Mutex` nor d.zig's may answer.
|
||||
_ = std.Thread.Mutex{};
|
||||
_ = std.mem.Allocator{ .ptr = undefined, .vtable = undefined };
|
||||
}
|
||||
|
||||
fn useLocal() void {
|
||||
_ = Mutex{};
|
||||
_ = d.Mutex{};
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
_ = useA();
|
||||
_ = useB();
|
||||
useMissing();
|
||||
useExternal();
|
||||
useLocal();
|
||||
}
|
||||
|
|
@ -684,9 +684,10 @@ describe.skipIf(!zigAvailable)('Zig function-local and anonymous containers (F8)
|
|||
// main.zig is a call, not a construction site.
|
||||
expect(reasonsOf('main', 'helper')).toHaveLength(1);
|
||||
expect(reasonsOf('main', 'helper')[0]).not.toContain('(constructor)');
|
||||
// No construction site is emitted as anything but CALLS, and every other
|
||||
// CALLS edge is an invocation.
|
||||
// Every marked edge targets a container type: a construction site can
|
||||
// never point at a callable.
|
||||
const constructionSites = calls.filter((e) => e.rel.reason.endsWith('(constructor)'));
|
||||
expect(constructionSites.length).toBeGreaterThan(0);
|
||||
expect(constructionSites.map((e) => e.targetLabel)).toEqual(
|
||||
constructionSites.map(() => 'Struct'),
|
||||
);
|
||||
|
|
@ -773,3 +774,73 @@ describe.skipIf(!zigAvailable)('Zig function-local and anonymous containers (F8)
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!zigAvailable)(
|
||||
'Zig qualified struct literals (`mod.T{…}`) as construction sites',
|
||||
() => {
|
||||
// The 2026-09-02 review re-test found that only same-file literals were
|
||||
// tracked: 163 `mod.Type{ … }` sites in a real project produced no CALLS
|
||||
// edge at all. A first attempt captured them as free constructors, which
|
||||
// resolve by the simple tail — `c.Thing{}` bound to a.zig's `Thing` although
|
||||
// c.zig defines none. Captured WITH the receiver they take the namespace
|
||||
// path `mod.fn()` takes, which resolves inside the module the receiver is
|
||||
// bound to: this suite pins the qualifier being honoured, not just the
|
||||
// edge appearing.
|
||||
let result: PipelineResult;
|
||||
let structCalls: ReturnType<typeof getRelationships>;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-qualified-literal'), () => {});
|
||||
structCalls = getRelationships(result, 'CALLS').filter((e) => e.targetLabel === 'Struct');
|
||||
}, 60000);
|
||||
|
||||
const edgesFrom = (source: string): string[] =>
|
||||
structCalls
|
||||
.filter((e) => e.source === source)
|
||||
.map((e) => `${e.target} @ ${e.targetFilePath} [${e.rel.reason}]`)
|
||||
.sort();
|
||||
|
||||
it('binds each same-named `Thing` to the module its qualifier names, marked as a construction site', () => {
|
||||
expect(edgesFrom('useA')).toEqual(['Thing @ src/a.zig [import-resolved (constructor)]']);
|
||||
expect(edgesFrom('useB')).toEqual(['Thing @ src/b.zig [import-resolved (constructor)]']);
|
||||
});
|
||||
|
||||
it('emits nothing when the qualifier’s module has no such member (`c.Thing{}`)', () => {
|
||||
// A tail-only resolution would have picked a.zig's or b.zig's `Thing`.
|
||||
expect(edgesFrom('useMissing')).toEqual([]);
|
||||
});
|
||||
|
||||
it('emits nothing for an external qualifier (`std.Thread.Mutex{}`), even with a same-named local and imported `Mutex`', () => {
|
||||
expect(edgesFrom('useExternal')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the same-file literal and the single-hop qualified literal apart by reason vocabulary', () => {
|
||||
expect(edgesFrom('useLocal')).toEqual([
|
||||
'Mutex @ src/d.zig [import-resolved (constructor)]',
|
||||
'Mutex @ src/main.zig [local-call (constructor)]',
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe.skipIf(!zigAvailable)(
|
||||
'Zig qualified struct literals — zig-basic (`pioneer.Pioneer{…}`, `pioneer.Tag{…}`)',
|
||||
() => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-basic'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('tracks a qualified struct AND union literal as marked construction sites', () => {
|
||||
const marked = getRelationships(result, 'CALLS')
|
||||
.filter((e) => e.source === 'main' && e.rel.reason.endsWith('(constructor)'))
|
||||
.map((e) => `${e.target}:${e.targetLabel} [${e.rel.reason}]`)
|
||||
.sort();
|
||||
expect(marked).toEqual([
|
||||
'Pioneer:Struct [import-resolved (constructor)]',
|
||||
'Tag:Union [import-resolved (constructor)]',
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue