mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(zig): bind container field types so self.field.m() resolves (F5)
A container's field types were never bound on its Class scope: the scope query's `container_field` rule captured only the name, and `emitZigScopeCaptures` synthesized no `@type-binding.field` group. The compound resolver reads member types from that scope (`typeOfMemberOnClass` → `classScope.typeBindings.get(field)`), so `self.session.name()`, `self.counter.incr()` — Lightpanda's dominant cross-object call shape — resolved 9 of 2803 times (0.3 %). - query.ts: capture `type: (_)? @declaration.field-type` on `container_field` (enum variants have none). - captures.ts: per typed field, push a `@type-binding.field` group (name = field, type = the type text, `@This()` aliases rewritten to the container name like parameter types); anonymous inline containers are skipped. The binding lands on the container's Class scope — the file's Class scope for a file-struct — since `zigBindingScopeFor` hoists only declaration names. - query.ts/captures.ts: `const page = self.page;` / `var s = self.session;` one-level field aliases become `@type-binding.alias` bindings whose "type" is the RHS path; the resolver's member-alias branch re-resolves it as a receiver chain. Import aliases (`const Counter = counter.Counter;`) are dropped — they are named imports. - interpret.ts: `@type-binding.field` → 'annotation', `@type-binding.alias` → 'assignment-inferred' (an annotation on the same binding wins). Corpus (Lightpanda, zig-corpus-check): self.field.m() 11/2803 (0.4 %) → 1433/2803 (51.1 %); ident.m() bound=local-field-access 29/1902 (1.5 %) → 307/1902 (16.1 %); chain.m() 146/5022 (2.9 %) → 642/5022 (12.8 %); CALLS 15838 → 18066. self.m() / free f() / ns.f() unchanged. Tests: unit (zig-extractors) — one @type-binding.field per typed field with sigils stripped and aliases rewritten; the binding hosted on the container's Class scope (file-struct: the file's Class scope, not Module) with the written spelling as declaredSpelling; field aliases bound to the RHS path and never for import aliases. Integration (zig-idioms `holder.zig`, zig-filestruct `Page.zig`): `viaField → incr` ×2 into counter.zig, `viaAlias → get/twice`, `sessionName → name` / `sessionLabel → name` into Session.zig. All fail without the change. The optional-payload capture `if (self.opt) |c| c.incr()` is not asserted (F6).
This commit is contained in:
parent
b527266ce8
commit
81a6b62890
8 changed files with 308 additions and 5 deletions
|
|
@ -24,7 +24,9 @@
|
|||
* to a namespace import, member forms to named/alias imports and
|
||||
* `usingnamespace` to a wildcard; receiver types come from `self`
|
||||
* parameters, `T{…}` / `mod.T{…}` / `List(u8){…}` literals, `T.init()`
|
||||
* call returns and `x: T` annotations (incl. decl literals). The
|
||||
* call returns, `x: T` annotations (incl. decl literals), container
|
||||
* FIELD types on the container's class scope (`self.session.name()`)
|
||||
* and one-level field aliases (`const s = self.session; s.name()`). The
|
||||
* emit-side wiring lives in `zig/scope-resolver.ts` (SCOPE_RESOLVERS).
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -522,6 +522,14 @@ export function emitZigScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
|
||||
// F5 — `const page = self.page;` value aliases (`@type-binding.alias`)
|
||||
// share the query shape with `const Counter = counter.Counter;`, which is
|
||||
// a NAMED IMPORT of `Counter` (promoted above) — a type binding beside it
|
||||
// would make `Counter` look like an instance of the class it names. Drop
|
||||
// the alias binding for those declarations.
|
||||
const aliasBinding = nodeMap['@type-binding.alias'];
|
||||
if (aliasBinding !== undefined && aliasDeclIds.has(aliasBinding.id)) continue;
|
||||
|
||||
// `@This()` aliases in type position — `self: *Self` in a container that
|
||||
// declares `const Self = @This();`, or `self: *SigHandler` in
|
||||
// `Sighandler.zig` (`const SigHandler = @This();`) — name the enclosing
|
||||
|
|
@ -586,6 +594,45 @@ export function emitZigScopeCaptures(
|
|||
|
||||
out.push(grouped);
|
||||
|
||||
// F5 — field types. `session: *Session,` declares the TYPE of the member
|
||||
// `session`; the compound resolver reads member types from the owning
|
||||
// container's Class scope (`typeOfMemberOnClass` →
|
||||
// `classScope.typeBindings.get('session')`), so without a binding there
|
||||
// `self.session.name()` / `self.counter.incr()` never resolve (Lightpanda:
|
||||
// 9 of 2803 such calls, 0.3 %). Synthesize a `@type-binding.field` group
|
||||
// per typed field (Go does the same in go/captures.ts; Rust/C++ capture it
|
||||
// in the query). Enum variants carry no type and get no binding. The
|
||||
// group's anchor is the type node — inside the container, never equal to
|
||||
// its range — so the extractor hosts it in the container's Class scope
|
||||
// (the file's Class scope for a file-struct); `zigBindingScopeFor` hoists
|
||||
// only declaration NAMES, never `@type-binding.*`. `?*Self` /
|
||||
// `?*Page` (a `@This()` alias) is rewritten to the container name exactly
|
||||
// like a parameter type; `normalizeZigTypeName` reduces `?*Session`,
|
||||
// `[]const Counter` to the nominal name and the extractor keeps the
|
||||
// written spelling as `declaredSpelling`.
|
||||
const fieldAnchor = nodeMap['@declaration.field'];
|
||||
const fieldType = nodeMap['@declaration.field-type'];
|
||||
const fieldName = nodeMap['@declaration.name'];
|
||||
if (
|
||||
fieldAnchor !== undefined &&
|
||||
fieldType !== undefined &&
|
||||
fieldName !== undefined &&
|
||||
// An inline anonymous container (`opts: struct { a: u32 },`) names no
|
||||
// type; its own Class scope already owns its members.
|
||||
!ZIG_CONTAINER_TYPES.has(fieldType.type)
|
||||
) {
|
||||
const rewritten =
|
||||
thisAliases.size > 0 ? rewriteZigThisAlias(fieldType, thisAliases) : undefined;
|
||||
out.push({
|
||||
'@type-binding.field': nodeToCapture('@type-binding.field', fieldType),
|
||||
'@type-binding.name': nodeToCapture('@type-binding.name', fieldName),
|
||||
'@type-binding.type':
|
||||
rewritten === undefined
|
||||
? nodeToCapture('@type-binding.type', fieldType)
|
||||
: syntheticCapture('@type-binding.type', fieldType, rewritten),
|
||||
});
|
||||
}
|
||||
|
||||
// `const Page = @import("Page.zig")` binds BOTH the module (namespace:
|
||||
// `Page.init()`) and, when the target is a file-struct, the type it
|
||||
// declares (`page: *Page`, `var p: Page`). The type is exported from the
|
||||
|
|
|
|||
|
|
@ -121,6 +121,19 @@ export function interpretZigTypeBinding(captures: CaptureMatch): ParsedTypeBindi
|
|||
} else if (captures['@type-binding.annotation'] !== undefined) {
|
||||
// `var x: T = undefined;` / `const x: T = .init(…);` — the declared type.
|
||||
source = 'annotation';
|
||||
} else if (captures['@type-binding.field'] !== undefined) {
|
||||
// `session: *Session,` — a container field's declared type, hosted in
|
||||
// the container's Class scope so `self.session.name()` walks it
|
||||
// (synthesized by `emitZigScopeCaptures`, F5). A declaration, hence
|
||||
// 'annotation': it must outrank nothing and be outranked by nothing —
|
||||
// a field has exactly one type source.
|
||||
source = 'annotation';
|
||||
} else if (captures['@type-binding.alias'] !== undefined) {
|
||||
// `const page = self.page;` — the RHS member path IS the "type"; the
|
||||
// compound resolver's member-alias branch re-resolves `self.page` as a
|
||||
// receiver chain (F5). Weakest source: an annotation on the same
|
||||
// binding (`const p: *Page = self.page;`) must win.
|
||||
source = 'assignment-inferred';
|
||||
}
|
||||
|
||||
return { boundName: name, rawTypeName: normalizeZigTypeName(type), source };
|
||||
|
|
|
|||
|
|
@ -94,9 +94,14 @@ const ZIG_SCOPE_QUERY = `
|
|||
|
||||
;; Declarations — container fields (struct fields, enum/union variants).
|
||||
;; The #not-eq? guard drops the MISSING placeholder identifier tree-sitter-zig
|
||||
;; recovers for an empty container body (see ZIG_QUERIES).
|
||||
;; recovers for an empty container body (see ZIG_QUERIES). The optional
|
||||
;; \`type:\` (absent on enum variants) is captured as @declaration.field-type:
|
||||
;; \`emitZigScopeCaptures\` turns it into a @type-binding.field on the
|
||||
;; container's Class scope so \`self.session.name()\` can walk the field's
|
||||
;; type (Rust/Go parity — see the F5 block in captures.ts).
|
||||
((container_field
|
||||
name: (identifier) @declaration.name) @declaration.field
|
||||
name: (identifier) @declaration.name
|
||||
type: (_)? @declaration.field-type) @declaration.field
|
||||
(#not-eq? @declaration.name ""))
|
||||
|
||||
;; Declarations — const/var bindings (import/container groups filtered in TS).
|
||||
|
|
@ -270,6 +275,28 @@ const ZIG_SCOPE_QUERY = `
|
|||
function: (field_expression
|
||||
object: (_) @type-binding.type)) .) @type-binding.call-return
|
||||
|
||||
;; Type bindings — field-access alias: \`const page = self.page;\` /
|
||||
;; \`var s = self.session;\` (F5 companion). The RHS path is kept verbatim as
|
||||
;; the "type" (\`self.page\`): the compound resolver's member-alias branch
|
||||
;; re-resolves it as a receiver chain (head \`self\` → class → field type),
|
||||
;; so \`page.getArena()\` dispatches like \`self.page.getArena()\` does. One
|
||||
;; level only — \`std.mem.Allocator\` chains and the namespace aliases
|
||||
;; \`const Counter = counter.Counter;\` (promoted to named imports) are not
|
||||
;; value aliases; \`emitZigScopeCaptures\` drops the import-alias matches.
|
||||
;; The trailing \`.\` anchor rejects a declaration whose value has any
|
||||
;; further child (\`const x = a.b + 1\` is a binary_expression, not matched
|
||||
;; anyway; the anchor is the belt to that brace).
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(field_expression
|
||||
object: (identifier)
|
||||
member: (identifier)) @type-binding.type .) @type-binding.alias
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(field_expression
|
||||
object: (identifier)
|
||||
member: (identifier)) @type-binding.type .) @type-binding.alias
|
||||
|
||||
;; References — free calls: foo(...)
|
||||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
|
|
|||
|
|
@ -21,3 +21,15 @@ pub fn bump(self: *Page) void {
|
|||
self.count += 1;
|
||||
_ = self.getArena();
|
||||
}
|
||||
|
||||
// Field-typed receivers (F5): `session: *Session` types the member, so a
|
||||
// call through it dispatches into Session.zig — directly (`self.session.name()`)
|
||||
// or through a local alias of the field (`const s = self.session; s.name()`).
|
||||
pub fn sessionName(self: *Page) []const u8 {
|
||||
return self.session.name();
|
||||
}
|
||||
|
||||
pub fn sessionLabel(self: *Page) []const u8 {
|
||||
const s = self.session;
|
||||
return s.name();
|
||||
}
|
||||
|
|
|
|||
27
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/holder.zig
vendored
Normal file
27
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/holder.zig
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Field-typed receivers (F5). Lightpanda writes most cross-object calls as
|
||||
// `self.<field>.<method>()` (`self.session.findFrame…`, `self.counter.incr()`),
|
||||
// or aliases the field first (`const page = self.page; page.getArena()`).
|
||||
// The receiver's type is the FIELD's declared type — plain, pointer, optional
|
||||
// pointer, slice — and lives on the owning container, not in any function.
|
||||
const Counter = @import("counter.zig").Counter;
|
||||
|
||||
pub const Holder = struct {
|
||||
counter: Counter,
|
||||
ptr: *Counter,
|
||||
opt: ?*Counter,
|
||||
many: []Counter,
|
||||
|
||||
pub fn viaField(self: *Holder) void {
|
||||
self.counter.incr();
|
||||
self.ptr.incr();
|
||||
// payload capture of an optional field
|
||||
if (self.opt) |c| c.incr();
|
||||
}
|
||||
|
||||
pub fn viaAlias(self: *Holder) u32 {
|
||||
const c = self.counter;
|
||||
var p = self.ptr;
|
||||
p.twice();
|
||||
return c.get();
|
||||
}
|
||||
};
|
||||
|
|
@ -323,6 +323,31 @@ describe.skipIf(!zigAvailable)('Zig idioms (zig-idioms fixture)', () => {
|
|||
.length,
|
||||
).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('types a receiver by the FIELD it is read from (`self.counter.incr()`, `self.ptr.incr()`) — F5', () => {
|
||||
// A container's field types were never bound on its Class scope, so the
|
||||
// compound resolver (`typeOfMemberOnClass`) found nothing for `counter`
|
||||
// and `self.<field>.<method>()` — Lightpanda's dominant cross-object call
|
||||
// shape — resolved 9 of 2803 times (0.3 %). Plain, pointer and optional-
|
||||
// pointer field types all reduce to the nominal `Counter`.
|
||||
const viaField = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.source === 'viaField' && e.target === 'incr',
|
||||
);
|
||||
// `self.counter.incr()` and `self.ptr.incr()` — two sites, one callee, and
|
||||
// the callee lives in counter.zig, not in holder.zig.
|
||||
expect(viaField.length).toBeGreaterThanOrEqual(2);
|
||||
expect(viaField.every((e) => e.targetFilePath.endsWith('counter.zig'))).toBe(true);
|
||||
// (`if (self.opt) |c| c.incr()` — the payload capture — is F6 territory
|
||||
// and is deliberately not asserted here.)
|
||||
});
|
||||
|
||||
it('types a local ALIAS of a field (`const c = self.counter; c.get()`, `var p = self.ptr; p.twice()`) — F5', () => {
|
||||
// `const c = self.counter;` binds nothing on the scope side without the
|
||||
// alias rule; the alias keeps the RHS path (`self.counter`) as its type
|
||||
// and the compound resolver re-resolves it as a receiver chain.
|
||||
expect(calls).toContain('viaAlias → get');
|
||||
expect(calls).toContain('viaAlias → twice');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -435,4 +460,19 @@ describe.skipIf(!zigAvailable)('Zig file-structs (zig-filestruct fixture)', () =
|
|||
const nameCall = calls.filter((e) => e.target === 'name');
|
||||
expect(nameCall.every((e) => e.targetFilePath.endsWith('Session.zig'))).toBe(true);
|
||||
});
|
||||
|
||||
it('dispatches through a file-struct FIELD typed by an imported file-struct (`self.session.name()`) — F5', () => {
|
||||
// `session: *Session` sits at the top level of Page.zig, whose Class
|
||||
// scope spans the file: the field's type binding must land there (not be
|
||||
// hoisted to the Module scope like the member NAMES are), and `Session`
|
||||
// must resolve through the import binding's type twin. Before F5 the
|
||||
// scope had no typeBindings at all and neither call resolved.
|
||||
const calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
expect(calls).toContain('sessionName → name');
|
||||
// `const s = self.session; s.name()` — alias of the field
|
||||
expect(calls).toContain('sessionLabel → name');
|
||||
const targets = getRelationships(result, 'CALLS').filter((e) => e.target === 'name');
|
||||
expect(targets.length).toBeGreaterThanOrEqual(2);
|
||||
expect(targets.every((e) => e.targetFilePath.endsWith('Session.zig'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -307,9 +307,12 @@ pub fn main() void {
|
|||
it('formals skip the leading `self` receiver so the member-call actual at 0 joins `cb`', () => {
|
||||
const runFormals = flow()
|
||||
.filter(
|
||||
(m) => m['@callable-flow.formal'] !== undefined && m['@callable-flow.owner']!.text === 'run',
|
||||
(m) =>
|
||||
m['@callable-flow.formal'] !== undefined && m['@callable-flow.owner']!.text === 'run',
|
||||
)
|
||||
.map((m) => `${m['@callable-flow.binding']!.text}@${m['@callable-flow.parameter-index']!.text}`);
|
||||
.map(
|
||||
(m) => `${m['@callable-flow.binding']!.text}@${m['@callable-flow.parameter-index']!.text}`,
|
||||
);
|
||||
expect(runFormals).toEqual(['cb@0']);
|
||||
});
|
||||
|
||||
|
|
@ -972,3 +975,135 @@ const lp = @import("lightpanda");
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
describeZig(
|
||||
'Zig field types (F5: `self.field.m()` resolves through the field’s declared type)',
|
||||
() => {
|
||||
// Lightpanda's dominant cross-object call shape is `self.<field>.<method>()`
|
||||
// (2803 sites) — and it resolved 9 times, because a container's field types
|
||||
// were never bound on its Class scope: `typeOfMemberOnClass` reads
|
||||
// `classScope.typeBindings.get(field)` and found nothing.
|
||||
const SRC = `
|
||||
const Page = @This();
|
||||
const Session = @import("Session.zig");
|
||||
session: *Session,
|
||||
parent: ?*Page,
|
||||
count: u32 = 0,
|
||||
pub const Holder = struct {
|
||||
const Self = @This();
|
||||
counter: Counter,
|
||||
ptr: *Counter,
|
||||
opt: ?*Counter,
|
||||
list: []const Counter,
|
||||
gen: std.ArrayList(u8),
|
||||
next: ?*Self,
|
||||
inline_: struct { a: u32 },
|
||||
pub fn viaField(self: *Holder) void { self.counter.incr(); }
|
||||
};
|
||||
pub const Kind = enum { a, b };
|
||||
pub const Payload = union(enum) { x: u32, y: Counter };
|
||||
`;
|
||||
|
||||
it('emits one @type-binding.field per TYPED field — the nominal type, sigils stripped, aliases rewritten', () => {
|
||||
const fields = emitZigScopeCaptures(SRC, 'src/Page.zig')
|
||||
.filter((m) => m['@type-binding.field'] !== undefined)
|
||||
.map((m) => {
|
||||
const parsed = interpretZigTypeBinding(m)!;
|
||||
return [
|
||||
parsed.boundName,
|
||||
m['@type-binding.type']!.text,
|
||||
parsed.rawTypeName,
|
||||
parsed.source,
|
||||
];
|
||||
});
|
||||
expect(fields).toEqual([
|
||||
// file-struct fields; `?*Page` is the file's own @This() alias, kept as
|
||||
// the file stem (Page.zig → Page) exactly like a parameter type
|
||||
['session', '*Session', 'Session', 'annotation'],
|
||||
['parent', '?*Page', 'Page', 'annotation'],
|
||||
['count', 'u32', 'u32', 'annotation'],
|
||||
// nested container: plain / pointer / optional pointer / slice /
|
||||
// generic instantiation / nested `Self` alias
|
||||
['counter', 'Counter', 'Counter', 'annotation'],
|
||||
['ptr', '*Counter', 'Counter', 'annotation'],
|
||||
['opt', '?*Counter', 'Counter', 'annotation'],
|
||||
['list', '[]const Counter', 'Counter', 'annotation'],
|
||||
['gen', 'std.ArrayList(u8)', 'std.ArrayList', 'annotation'],
|
||||
['next', '?*Holder', 'Holder', 'annotation'],
|
||||
// the anonymous inline struct's OWN field, not `inline_` itself
|
||||
['a', 'u32', 'u32', 'annotation'],
|
||||
// union variants carry a type; enum variants (`a`, `b`) do not
|
||||
['x', 'u32', 'u32', 'annotation'],
|
||||
['y', 'Counter', 'Counter', 'annotation'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('hosts the binding on the CONTAINER’s Class scope — the file’s Class scope for a file-struct — never hoisted to Module', () => {
|
||||
// `zigBindingScopeFor` hoists member NAMES of a file-struct to the Module
|
||||
// scope so `Page.init()` keeps working; the compound resolver reads member
|
||||
// TYPES from the Class scope, so those must stay put.
|
||||
const parsed = extractScopes(
|
||||
emitZigScopeCaptures(SRC, 'src/Page.zig'),
|
||||
'src/Page.zig',
|
||||
zigProvider,
|
||||
);
|
||||
const byKind = (kind: string) => parsed.scopes.filter((s) => s.kind === kind);
|
||||
const moduleScope = byKind('Module')[0]!;
|
||||
expect(moduleScope.typeBindings.size).toBe(0);
|
||||
const fileClass = byKind('Class').find(
|
||||
(s) =>
|
||||
s.range.startLine === moduleScope.range.startLine &&
|
||||
s.range.endLine === moduleScope.range.endLine,
|
||||
)!;
|
||||
expect(fileClass).toBeDefined();
|
||||
expect(fileClass.typeBindings.get('session')?.rawName).toBe('Session');
|
||||
// The written spelling survives beside the reduced name (`TypeRef.declaredSpelling`).
|
||||
expect(fileClass.typeBindings.get('session')?.declaredSpelling).toBe('*Session');
|
||||
expect(fileClass.typeBindings.get('parent')?.rawName).toBe('Page');
|
||||
const holder = byKind('Class').find((s) => s.typeBindings.has('counter'))!;
|
||||
expect(holder).toBeDefined();
|
||||
expect(holder.id).not.toBe(fileClass.id);
|
||||
expect(holder.typeBindings.get('opt')).toMatchObject({
|
||||
rawName: 'Counter',
|
||||
declaredSpelling: '?*Counter',
|
||||
});
|
||||
expect(holder.typeBindings.get('next')?.rawName).toBe('Holder');
|
||||
// The receiver `self` stays on the function scope, not on the class.
|
||||
expect(holder.typeBindings.has('self')).toBe(false);
|
||||
expect(byKind('Function')[0]!.typeBindings.get('self')?.rawName).toBe('Holder');
|
||||
});
|
||||
|
||||
it('binds a local alias of a field to the RHS path (`const page = self.page;`), never an import alias', () => {
|
||||
const src = `
|
||||
const counter = @import("counter.zig");
|
||||
const Counter = counter.Counter;
|
||||
const Allocator = std.mem.Allocator;
|
||||
pub fn run(self: *Holder) void {
|
||||
const page = self.page;
|
||||
var s = self.session;
|
||||
const typed: *Page = self.page;
|
||||
_ = page; _ = s; _ = typed;
|
||||
}
|
||||
`;
|
||||
const aliases = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@type-binding.alias'] !== undefined)
|
||||
.map((m) => interpretZigTypeBinding(m));
|
||||
expect(aliases).toEqual([
|
||||
{ boundName: 'page', rawTypeName: 'self.page', source: 'assignment-inferred' },
|
||||
{ boundName: 's', rawTypeName: 'self.session', source: 'assignment-inferred' },
|
||||
{ boundName: 'typed', rawTypeName: 'self.page', source: 'assignment-inferred' },
|
||||
]);
|
||||
// `const Counter = counter.Counter;` is a NAMED IMPORT (counter is an
|
||||
// @import binding), and `std.mem.Allocator` is a two-level chain — neither
|
||||
// is a value alias.
|
||||
expect(aliases.map((a) => a!.boundName)).not.toContain('Counter');
|
||||
expect(aliases.map((a) => a!.boundName)).not.toContain('Allocator');
|
||||
// The annotation outranks the alias for the same name.
|
||||
const parsed = extractScopes(emitZigScopeCaptures(src, 'x.zig'), 'x.zig', zigProvider);
|
||||
const fn = parsed.scopes.find((s) => s.kind === 'Function')!;
|
||||
const block = parsed.scopes.find((s) => s.kind === 'Block')!;
|
||||
const typed = block.typeBindings.get('typed') ?? fn.typeBindings.get('typed');
|
||||
expect(typed).toMatchObject({ rawName: 'Page', source: 'annotation' });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue