mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-10 22:43:40 +00:00
fix(zig): address thirteenth gitnexus-check review pass
- File-struct receivers named after the file stem were always static: the
method builder called `isStatic` / `extractReceiverType` /
`extractParameters` without the extractor context's `filePath`, so
`zigReceiverParameter` could not name a file-struct (`fn add(ledger:
*Ledger)` in `Ledger.zig`, no `Self` alias) and the fn came out static
with the receiver in its arity (`Ledger.add#2`) — an id the scope side,
which always has the path, never produces, so its CALLS edges went
nowhere. `MethodExtractionConfig` now passes `filePath` as an optional
trailing argument to those three hooks (same shape as
`extractOwnerName`); the Zig config threads it through, every other
config ignores it. Regression tests in `zig-extractors.test.ts` (unit)
and `resolvers/zig.test.ts` (new `Ledger.zig` in the `zig-receivers`
fixture: ids, `isStatic`, and the three CALLS edges); both fail on the
previous source.
- `LINKABLE_LABELS` comment: the remaining `CLASS_KINDS` entries include
`Namespace`.
Not re-fixed:
- "Ownerless-method assertion regex cannot match `.zig` graph IDs": the
`[^:]+` segment consumes the whole file path (dots included) up to the
second colon, and `[^.]+#\d+$` then matches only an owner-less name —
`Method:src/Sorter.zig:lessThan#3` → true, `…:Sorter.sortBoth$1.lessThan#3`
→ false, checked with node.
- "Public namespace imports are never marked as re-exports": the shared
`ParsedImport` namespace variant has no `reexportsName` field and
`contributesReexportEdge` excludes namespace drafts on `base.kind` by
contract; a `pub const X = @import("x.zig")` hub member is exposed
through `findExportedDefIncludingImportedNames` instead, which is what
the audit commit added for exactly that shape.
- "Private Zig namespace imports are treated as public hub exports": a
private import cannot be named through the hub in code that compiles,
and `findExportedDef` applies the same no-visibility rule to local
defs; the finalized binding channel carries no `pub` bit to check.
- "Range binding mutates finalized scopes" and "unconditionally adds an
optional Zig grammar to the fixture suite": refuted in the tenth and
eleventh pass notes of the PR body, unchanged since.
This commit is contained in:
parent
f121e539ed
commit
34c5347323
8 changed files with 110 additions and 16 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -312,8 +312,8 @@ export const LINKABLE_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
|
|||
//
|
||||
// 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',
|
||||
|
|
|
|||
12
gitnexus/test/fixtures/lang-resolution/zig-receivers/src/Ledger.zig
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/zig-receivers/src/Ledger.zig
vendored
Normal file
|
|
@ -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 .{};
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, boolean>();
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue