gitnexus 1.6.12-rc.20 → 1.6.12-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/eval-server.js +13 -1
- package/dist/core/ingestion/language-config.d.ts +82 -13
- package/dist/core/ingestion/language-config.js +217 -13
- package/dist/core/ingestion/languages/typescript/query.d.ts +31 -0
- package/dist/core/ingestion/languages/typescript/query.js +7 -1
- package/dist/core/ingestion/languages/zig/query.d.ts +26 -0
- package/dist/core/ingestion/languages/zig/query.js +61 -1
- package/dist/core/ingestion/languages/zig/range-binding.d.ts +8 -0
- package/dist/core/ingestion/languages/zig/range-binding.js +24 -0
- package/dist/core/ingestion/languages/zig/scope-resolver.d.ts +3 -2
- package/dist/core/ingestion/languages/zig/scope-resolver.js +16 -5
- package/dist/core/ingestion/languages/zig/this-alias-bindings.d.ts +58 -0
- package/dist/core/ingestion/languages/zig/this-alias-bindings.js +176 -0
- package/dist/core/ingestion/languages/zig/workspace-static-gating.js +9 -3
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +78 -5
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +262 -7
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +5 -1
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +35 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +76 -0
- package/dist/core/ingestion/scope-resolution/value-ref-edges.d.ts +14 -0
- package/dist/core/ingestion/scope-resolution/value-ref-edges.js +14 -0
- package/dist/mcp/local/local-backend.d.ts +33 -0
- package/dist/mcp/local/local-backend.js +151 -0
- package/dist/mcp/tools.js +6 -4
- package/dist/storage/parse-cache.js +18 -1
- package/package.json +1 -1
- package/web/assets/{agent-C2n33ANd.js → agent-DuBBkGz1.js} +35 -35
- package/web/assets/{index-CHJoW_P1.js → index-C2pinrQ1.js} +3 -3
- package/web/index.html +1 -1
package/dist/cli/eval-server.js
CHANGED
|
@@ -501,8 +501,20 @@ export function formatImpactResult(result) {
|
|
|
501
501
|
}
|
|
502
502
|
// #1858 — an interface / indirection boundary on the path makes this a lower
|
|
503
503
|
// bound; surface it so the count is not read as exhaustive.
|
|
504
|
+
//
|
|
505
|
+
// The header names no cause AND asserts no omitted caller, because it cannot
|
|
506
|
+
// know either. DI / dynamic dispatch was the only producer of `lower-bound`
|
|
507
|
+
// when this was written; #3399 added callables named in VALUE position (a
|
|
508
|
+
// registration table, a callback argument), and one of its producers is a
|
|
509
|
+
// probe that could not RUN — `callableValueReferenceBoundaries` hedges on a
|
|
510
|
+
// failed query and says in so many words that whether the symbol is
|
|
511
|
+
// registered is unknown. A header claiming "some callers are not traced"
|
|
512
|
+
// would there assert an omission nothing established, and would contradict
|
|
513
|
+
// the bullet printed directly under it. The bullets carry the cause — they
|
|
514
|
+
// are generated per-cause by `computeEpistemicBoundary` — so the header only
|
|
515
|
+
// has to say the count is a floor.
|
|
504
516
|
if (result.epistemic === 'lower-bound') {
|
|
505
|
-
lines.push('⚠️ Lower bound —
|
|
517
|
+
lines.push('⚠️ Lower bound — impact may be incomplete and actual impact may be higher:');
|
|
506
518
|
for (const b of result.boundaries || [])
|
|
507
519
|
lines.push(` • ${b}`);
|
|
508
520
|
}
|
|
@@ -71,13 +71,24 @@ export interface SwiftPackageConfig {
|
|
|
71
71
|
/** Zig package config parsed from build.zig.zon and the root build.zig */
|
|
72
72
|
export interface ZigBuildZonConfig {
|
|
73
73
|
/**
|
|
74
|
-
* Map of dependency name -> the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* `
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
74
|
+
* Map of dependency name -> the dep's directory, in one of two spellings
|
|
75
|
+
* depending on which package this config describes:
|
|
76
|
+
*
|
|
77
|
+
* - ROOT package (`pkg === ''`): the raw `.path = "..."` value, exactly as
|
|
78
|
+
* written in build.zig.zon (relative to the repo root, and possibly
|
|
79
|
+
* escaping it: `../local_dep`). This is what `parseZigBuildZon` promises
|
|
80
|
+
* and what its tests pin.
|
|
81
|
+
* - NESTED package: repo-relative and already normalized, because a nested
|
|
82
|
+
* package's `.path` is written relative to ITS directory and means
|
|
83
|
+
* nothing against the repo-relative keys consumers match on
|
|
84
|
+
* (`packages/app`'s `../core` is stored as `packages/core`). A dep
|
|
85
|
+
* escaping the REPO root is dropped rather than stored.
|
|
86
|
+
*
|
|
87
|
+
* Either spelling is safe to hand to `normalizeZigDepPath` below — it rejects
|
|
88
|
+
* absolute and repo-escaping values and is idempotent on an already
|
|
89
|
+
* normalized one, which is what `resolveZigImportInternal` relies on.
|
|
90
|
+
* `.url`-based deps cannot be resolved to a repo-local file (they unpack into
|
|
91
|
+
* a build cache outside the repo) and so are not included here.
|
|
81
92
|
*/
|
|
82
93
|
pathDeps: Map<string, string>;
|
|
83
94
|
/**
|
|
@@ -116,6 +127,27 @@ export interface ZigBuildZonConfig {
|
|
|
116
127
|
*/
|
|
117
128
|
buildModules?: readonly ZigBuildModule[];
|
|
118
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* One Zig build package: the directory whose `build.zig` / `build.zig.zon`
|
|
132
|
+
* declare the config, and that config with every path REPO-relative.
|
|
133
|
+
*
|
|
134
|
+
* A Zig module's import table is declared by the `build.zig` of the package it
|
|
135
|
+
* belongs to, so a repo holding several packages holds several import tables —
|
|
136
|
+
* the same shape a TypeScript monorepo has with a `tsconfig.json` per package.
|
|
137
|
+
*/
|
|
138
|
+
export interface ZigPackageScope {
|
|
139
|
+
/** Repo-relative directory the package governs (`''` for the repo root). */
|
|
140
|
+
readonly dir: string;
|
|
141
|
+
readonly config: ZigBuildZonConfig;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Every Zig build package in the repo, indexed so the nearest one to a file
|
|
145
|
+
* wins — the `TsconfigIndex` analogue, and for the same reason.
|
|
146
|
+
*/
|
|
147
|
+
export interface ZigWorkspaceIndex {
|
|
148
|
+
/** Deepest-first, so the first `dir` that prefixes a file path governs it. */
|
|
149
|
+
readonly packages: readonly ZigPackageScope[];
|
|
150
|
+
}
|
|
119
151
|
/** One build module of the root `build.zig` — see `ZigBuildZonConfig.buildModules`. */
|
|
120
152
|
export interface ZigBuildModule {
|
|
121
153
|
/** The `addModule("<name>", …)` name; absent for `createModule` bindings
|
|
@@ -189,14 +221,51 @@ export declare function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftP
|
|
|
189
221
|
* string literals — a commented-out `.path` or a `}` inside a comment
|
|
190
222
|
* or string cannot declare a dep or truncate the block.
|
|
191
223
|
*/
|
|
192
|
-
export declare function loadZigBuildConfig(repoRoot: string): Promise<ZigBuildZonConfig | null>;
|
|
224
|
+
export declare function loadZigBuildConfig(repoRoot: string, packageDir?: string): Promise<ZigBuildZonConfig | null>;
|
|
193
225
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
226
|
+
* The Zig build package governing `filePath` — the nearest one at or above it.
|
|
227
|
+
*
|
|
228
|
+
* A Zig module's import table is declared by the `build.zig` of the package the
|
|
229
|
+
* file belongs to, so the nearest enclosing package is the faithful reading of
|
|
230
|
+
* `@import("name")` at that site, exactly as `tsconfigFor` reads a non-relative
|
|
231
|
+
* specifier against the nearest enclosing project.
|
|
232
|
+
*
|
|
233
|
+
* There is deliberately NO fall-through to an enclosing package when the nearest
|
|
234
|
+
* one does not bind the name. Falling through is how a vendored dependency's
|
|
235
|
+
* `@import("config")` silently resolved to the outer repo's `config` module —
|
|
236
|
+
* the same failure `loadTsconfigIndex` documents for a package whose own
|
|
237
|
+
* tsconfig declares no `baseUrl`, and the same failure the per-module import
|
|
238
|
+
* tables in `resolveZigImportInternal` already exist to prevent one level down.
|
|
239
|
+
*/
|
|
240
|
+
export declare function zigPackageFor(index: ZigWorkspaceIndex | null | undefined, filePath: string): ZigBuildZonConfig | null;
|
|
241
|
+
/**
|
|
242
|
+
* Load every Zig build package in the repo, nearest-first.
|
|
243
|
+
*
|
|
244
|
+
* Called with no `packageDir` — which is how every call site read it before
|
|
245
|
+
* this function existed — `loadZigBuildConfig` reads the ROOT `build.zig` /
|
|
246
|
+
* `build.zig.zon` and nothing else. That is the whole configuration of a
|
|
247
|
+
* single-package repo and none of the configuration of a monorepo: a repo
|
|
248
|
+
* laying its packages out as `packages/<name>/build.zig` has no root build
|
|
249
|
+
* files at all, so the loader answers `null` and EVERY bare
|
|
250
|
+
* `@import("<module>")` in it goes unresolved — cross-file resolution silently
|
|
251
|
+
* degrades to relative imports only. Measured on a two-package fixture:
|
|
252
|
+
* `config = null`, `@import("core")` → `null`.
|
|
253
|
+
*
|
|
254
|
+
* The loader itself is not root-bound any more: this function is what supplies
|
|
255
|
+
* it a `packageDir`, one per package below.
|
|
256
|
+
*
|
|
257
|
+
* So the packages are discovered the way tsconfigs are (`findTsconfigFiles`):
|
|
258
|
+
* one bounded breadth-first walk that skips the hardcoded ignore set, then
|
|
259
|
+
* deepest-first ordering so `zigPackageFor` can take the first match.
|
|
260
|
+
*
|
|
261
|
+
* Called from `ScopeResolver.loadResolutionConfig`, which the orchestrator runs
|
|
262
|
+
* once per LANGUAGE workspace pass — so the walk happens only for repos that
|
|
263
|
+
* actually contain Zig. `loadImportConfigs`, which runs unconditionally for
|
|
264
|
+
* every repo, keeps calling `loadZigBuildConfig` for the root package alone;
|
|
265
|
+
* that is the same split TypeScript already has between the cheap
|
|
266
|
+
* `loadTsconfigPaths` and the repo-walking `loadTsconfigIndex`.
|
|
199
267
|
*/
|
|
268
|
+
export declare function loadZigWorkspaceIndex(repoRoot: string): Promise<ZigWorkspaceIndex | null>;
|
|
200
269
|
export declare function normalizeZigDepPath(depPath: string): string | null;
|
|
201
270
|
/**
|
|
202
271
|
* The `root_source_file` paths a `build.zig` declares, dep-relative, with the
|
|
@@ -3,6 +3,7 @@ import { createReadStream } from 'fs';
|
|
|
3
3
|
import { createInterface } from 'readline';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { isDev } from './utils/env.js';
|
|
6
|
+
import { isHardcodedIgnoredDirectoryAtPath } from '../../config/ignore-service.js';
|
|
6
7
|
import { mapConcurrent } from '../../lib/utils.js';
|
|
7
8
|
import { logger } from '../logger.js';
|
|
8
9
|
function normalizeComposerDirectory(baseDir, directory) {
|
|
@@ -475,10 +476,18 @@ export async function loadSwiftPackageConfig(repoRoot) {
|
|
|
475
476
|
* string literals — a commented-out `.path` or a `}` inside a comment
|
|
476
477
|
* or string cannot declare a dep or truncate the block.
|
|
477
478
|
*/
|
|
478
|
-
export async function loadZigBuildConfig(repoRoot) {
|
|
479
|
+
export async function loadZigBuildConfig(repoRoot, packageDir = '') {
|
|
480
|
+
// Every path this function returns is REPO-relative, because that is the
|
|
481
|
+
// keyspace `allFilePaths` uses. The parsers below answer package-relative, so
|
|
482
|
+
// a nested package rebases them through `inPackage`. For the root package
|
|
483
|
+
// (`packageDir === ''`) the prefix is empty and every value is byte-identical
|
|
484
|
+
// to what this function returned before nested packages existed.
|
|
485
|
+
const pkg = packageDir === '' ? '' : `${packageDir}/`;
|
|
486
|
+
const inPackage = (relToPackage) => `${pkg}${relToPackage}`;
|
|
487
|
+
const packageFile = (name) => path.join(repoRoot, packageDir, name);
|
|
479
488
|
let config = null;
|
|
480
489
|
try {
|
|
481
|
-
const raw = await fs.readFile(
|
|
490
|
+
const raw = await fs.readFile(packageFile('build.zig.zon'), 'utf-8');
|
|
482
491
|
config = parseZigBuildZon(raw);
|
|
483
492
|
}
|
|
484
493
|
catch {
|
|
@@ -490,10 +499,11 @@ export async function loadZigBuildConfig(repoRoot) {
|
|
|
490
499
|
let rootModules;
|
|
491
500
|
let rootBuildZig = null;
|
|
492
501
|
try {
|
|
493
|
-
rootBuildZig = await fs.readFile(
|
|
502
|
+
rootBuildZig = await fs.readFile(packageFile('build.zig'), 'utf-8');
|
|
494
503
|
const parsed = parseZigRootModules(rootBuildZig);
|
|
495
|
-
if (parsed.size > 0)
|
|
496
|
-
rootModules = parsed;
|
|
504
|
+
if (parsed.size > 0) {
|
|
505
|
+
rootModules = new Map(Array.from(parsed, ([name, root]) => [name, inPackage(root)]));
|
|
506
|
+
}
|
|
497
507
|
}
|
|
498
508
|
catch {
|
|
499
509
|
// No root build.zig — nothing to declare.
|
|
@@ -502,7 +512,7 @@ export async function loadZigBuildConfig(repoRoot) {
|
|
|
502
512
|
if (rootBuildZig === null)
|
|
503
513
|
return null;
|
|
504
514
|
// No zon: no path deps, so `dep.module(…)` operands resolve to nothing.
|
|
505
|
-
const buildModules = parseZigBuildModules(rootBuildZig);
|
|
515
|
+
const buildModules = rebaseZigBuildModules(parseZigBuildModules(rootBuildZig), inPackage);
|
|
506
516
|
if (!rootModules && buildModules.length === 0)
|
|
507
517
|
return null;
|
|
508
518
|
return {
|
|
@@ -519,10 +529,25 @@ export async function loadZigBuildConfig(repoRoot) {
|
|
|
519
529
|
// Per path dep: the modules its build.zig NAMES (`addModule("core", …)`),
|
|
520
530
|
// repo-relative — what a root-build.zig `dep.module("core")` operand means.
|
|
521
531
|
const depModules = new Map();
|
|
532
|
+
// A nested package's `.path` values are written relative to ITS directory, so
|
|
533
|
+
// they are rebased here and stored repo-relative; `resolveZigImportInternal`
|
|
534
|
+
// then reads them through the same `normalizeZigDepPath`, which is idempotent
|
|
535
|
+
// on an already-normalized value. A dep that escapes the REPO root (not merely
|
|
536
|
+
// the package) resolves to nothing and is dropped. The root package keeps its
|
|
537
|
+
// raw spelling, which is what `parseZigBuildZon` promises and its tests pin.
|
|
538
|
+
const pathDeps = pkg === '' ? config.pathDeps : new Map();
|
|
522
539
|
for (const [depName, depPath] of config.pathDeps) {
|
|
523
|
-
|
|
540
|
+
// Asked of the value AS WRITTEN, before the package prefix goes on: an
|
|
541
|
+
// absolute `.path` points outside the repository whichever package declared
|
|
542
|
+
// it, and prefixing hides that from `normalizeZigDepPath`. See
|
|
543
|
+
// `isAbsoluteZigDepPath`.
|
|
544
|
+
if (isAbsoluteZigDepPath(depPath))
|
|
545
|
+
continue;
|
|
546
|
+
const rel = normalizeZigDepPath(`${pkg}${depPath}`);
|
|
524
547
|
if (rel === null)
|
|
525
548
|
continue;
|
|
549
|
+
if (pkg !== '')
|
|
550
|
+
pathDeps.set(depName, rel);
|
|
526
551
|
let buildZig;
|
|
527
552
|
try {
|
|
528
553
|
buildZig = await fs.readFile(path.join(repoRoot, rel, 'build.zig'), 'utf-8');
|
|
@@ -542,14 +567,169 @@ export async function loadZigBuildConfig(repoRoot) {
|
|
|
542
567
|
if (named.size > 0)
|
|
543
568
|
depModules.set(depName, named);
|
|
544
569
|
}
|
|
545
|
-
const buildModules = rootBuildZig === null
|
|
570
|
+
const buildModules = rootBuildZig === null
|
|
571
|
+
? []
|
|
572
|
+
: rebaseZigBuildModules(parseZigBuildModules(rootBuildZig, depModules), inPackage, depModules);
|
|
546
573
|
return {
|
|
547
574
|
...config,
|
|
575
|
+
pathDeps,
|
|
548
576
|
...(moduleRoots.size > 0 ? { moduleRoots } : {}),
|
|
549
577
|
...(rootModules ? { rootModules } : {}),
|
|
550
578
|
...(buildModules.length > 0 ? { buildModules } : {}),
|
|
551
579
|
};
|
|
552
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Rebase a package's own build modules to repo-relative paths.
|
|
583
|
+
*
|
|
584
|
+
* `parseZigBuildModules` answers package-relative for everything it read out of
|
|
585
|
+
* the `build.zig` it was handed, with one exception: an alias resolved through
|
|
586
|
+
* `depModules` (`addImport("api", dep.module("core"))`) is already repo-relative,
|
|
587
|
+
* because `depModules` was built that way. Prefixing that a second time would
|
|
588
|
+
* point the alias at a path no file has. The already-repo-relative values are
|
|
589
|
+
* therefore identified by membership in `depModules`, not guessed at from their
|
|
590
|
+
* shape.
|
|
591
|
+
*/
|
|
592
|
+
function rebaseZigBuildModules(modules, inPackage, depModules) {
|
|
593
|
+
if (inPackage('') === '')
|
|
594
|
+
return [...modules];
|
|
595
|
+
const fromDep = new Set();
|
|
596
|
+
for (const named of depModules?.values() ?? [])
|
|
597
|
+
for (const root of named.values())
|
|
598
|
+
fromDep.add(root);
|
|
599
|
+
return modules.map((mod) => ({
|
|
600
|
+
...(mod.name !== undefined ? { name: mod.name } : {}),
|
|
601
|
+
root: inPackage(mod.root),
|
|
602
|
+
imports: new Map(Array.from(mod.imports, ([alias, root]) => [
|
|
603
|
+
alias,
|
|
604
|
+
fromDep.has(root) ? root : inPackage(root),
|
|
605
|
+
])),
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
/** Bounds for the package walk, mirroring the tsconfig scan. */
|
|
609
|
+
const ZIG_SCAN_MAX_DIRS = 20_000;
|
|
610
|
+
const ZIG_SCAN_MAX_DEPTH = 24;
|
|
611
|
+
/**
|
|
612
|
+
* The Zig build package governing `filePath` — the nearest one at or above it.
|
|
613
|
+
*
|
|
614
|
+
* A Zig module's import table is declared by the `build.zig` of the package the
|
|
615
|
+
* file belongs to, so the nearest enclosing package is the faithful reading of
|
|
616
|
+
* `@import("name")` at that site, exactly as `tsconfigFor` reads a non-relative
|
|
617
|
+
* specifier against the nearest enclosing project.
|
|
618
|
+
*
|
|
619
|
+
* There is deliberately NO fall-through to an enclosing package when the nearest
|
|
620
|
+
* one does not bind the name. Falling through is how a vendored dependency's
|
|
621
|
+
* `@import("config")` silently resolved to the outer repo's `config` module —
|
|
622
|
+
* the same failure `loadTsconfigIndex` documents for a package whose own
|
|
623
|
+
* tsconfig declares no `baseUrl`, and the same failure the per-module import
|
|
624
|
+
* tables in `resolveZigImportInternal` already exist to prevent one level down.
|
|
625
|
+
*/
|
|
626
|
+
export function zigPackageFor(index, filePath) {
|
|
627
|
+
if (index === null || index === undefined)
|
|
628
|
+
return null;
|
|
629
|
+
for (const scope of index.packages) {
|
|
630
|
+
if (scope.dir === '')
|
|
631
|
+
return scope.config;
|
|
632
|
+
if (filePath.startsWith(`${scope.dir}/`))
|
|
633
|
+
return scope.config;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Load every Zig build package in the repo, nearest-first.
|
|
639
|
+
*
|
|
640
|
+
* Called with no `packageDir` — which is how every call site read it before
|
|
641
|
+
* this function existed — `loadZigBuildConfig` reads the ROOT `build.zig` /
|
|
642
|
+
* `build.zig.zon` and nothing else. That is the whole configuration of a
|
|
643
|
+
* single-package repo and none of the configuration of a monorepo: a repo
|
|
644
|
+
* laying its packages out as `packages/<name>/build.zig` has no root build
|
|
645
|
+
* files at all, so the loader answers `null` and EVERY bare
|
|
646
|
+
* `@import("<module>")` in it goes unresolved — cross-file resolution silently
|
|
647
|
+
* degrades to relative imports only. Measured on a two-package fixture:
|
|
648
|
+
* `config = null`, `@import("core")` → `null`.
|
|
649
|
+
*
|
|
650
|
+
* The loader itself is not root-bound any more: this function is what supplies
|
|
651
|
+
* it a `packageDir`, one per package below.
|
|
652
|
+
*
|
|
653
|
+
* So the packages are discovered the way tsconfigs are (`findTsconfigFiles`):
|
|
654
|
+
* one bounded breadth-first walk that skips the hardcoded ignore set, then
|
|
655
|
+
* deepest-first ordering so `zigPackageFor` can take the first match.
|
|
656
|
+
*
|
|
657
|
+
* Called from `ScopeResolver.loadResolutionConfig`, which the orchestrator runs
|
|
658
|
+
* once per LANGUAGE workspace pass — so the walk happens only for repos that
|
|
659
|
+
* actually contain Zig. `loadImportConfigs`, which runs unconditionally for
|
|
660
|
+
* every repo, keeps calling `loadZigBuildConfig` for the root package alone;
|
|
661
|
+
* that is the same split TypeScript already has between the cheap
|
|
662
|
+
* `loadTsconfigPaths` and the repo-walking `loadTsconfigIndex`.
|
|
663
|
+
*/
|
|
664
|
+
export async function loadZigWorkspaceIndex(repoRoot) {
|
|
665
|
+
const dirs = await findZigPackageDirs(repoRoot);
|
|
666
|
+
if (dirs.length === 0)
|
|
667
|
+
return null;
|
|
668
|
+
const packages = [];
|
|
669
|
+
for (const dir of dirs) {
|
|
670
|
+
const config = await loadZigBuildConfig(repoRoot, dir);
|
|
671
|
+
// A `build.zig` that declares no module and no path dep contributes nothing
|
|
672
|
+
// a lookup could answer with. Keeping it as an empty scope would be worse
|
|
673
|
+
// than dropping it: it would shadow an enclosing package that DOES declare
|
|
674
|
+
// the name, and answer nothing in its place.
|
|
675
|
+
if (config !== null)
|
|
676
|
+
packages.push({ dir, config });
|
|
677
|
+
}
|
|
678
|
+
if (packages.length === 0)
|
|
679
|
+
return null;
|
|
680
|
+
// Deepest first, so `zigPackageFor` takes the most specific package rather
|
|
681
|
+
// than whichever the walk reached first.
|
|
682
|
+
packages.sort((a, b) => b.dir.length - a.dir.length || a.dir.localeCompare(b.dir));
|
|
683
|
+
return { packages };
|
|
684
|
+
}
|
|
685
|
+
/** Repo-relative directories holding a `build.zig` and/or a `build.zig.zon`. */
|
|
686
|
+
async function findZigPackageDirs(repoRoot) {
|
|
687
|
+
const found = [];
|
|
688
|
+
const queue = [{ dir: repoRoot, depth: 0 }];
|
|
689
|
+
// A HEAD INDEX rather than `queue.shift()`. The queue is pushed to while it is
|
|
690
|
+
// drained, which keeps the array in a mode where `shift()` memmoves the whole
|
|
691
|
+
// remainder instead of taking V8's left-trimming fast path — so the walk is
|
|
692
|
+
// quadratic in the frontier, and `ZIG_SCAN_MAX_DIRS` is the bound on how bad
|
|
693
|
+
// that gets. Measured at that bound (20,000 dequeues): 53 ms at fan-out 4 and
|
|
694
|
+
// 81 ms at fan-out 20, against 0.8 ms here — 66-106x, paid before any config
|
|
695
|
+
// is read. Memory is unchanged: entries were already retained by the pushes,
|
|
696
|
+
// `shift()` only dropped the head.
|
|
697
|
+
let queueHead = 0;
|
|
698
|
+
let dirsScanned = 0;
|
|
699
|
+
while (queueHead < queue.length && dirsScanned < ZIG_SCAN_MAX_DIRS) {
|
|
700
|
+
const { dir, depth } = queue[queueHead++];
|
|
701
|
+
dirsScanned++;
|
|
702
|
+
let entries;
|
|
703
|
+
try {
|
|
704
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
let isPackage = false;
|
|
710
|
+
for (const entry of entries) {
|
|
711
|
+
if (entry.isDirectory()) {
|
|
712
|
+
const childDir = path.join(dir, entry.name);
|
|
713
|
+
if (isHardcodedIgnoredDirectoryAtPath(repoRoot, childDir))
|
|
714
|
+
continue;
|
|
715
|
+
if (depth < ZIG_SCAN_MAX_DEPTH)
|
|
716
|
+
queue.push({ dir: childDir, depth: depth + 1 });
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (!entry.isFile())
|
|
720
|
+
continue;
|
|
721
|
+
// Either marker declares a package: a `build.zig` with no zon still names
|
|
722
|
+
// modules, and a zon with no build.zig still names path deps.
|
|
723
|
+
if (entry.name === 'build.zig' || entry.name === 'build.zig.zon')
|
|
724
|
+
isPackage = true;
|
|
725
|
+
}
|
|
726
|
+
if (isPackage) {
|
|
727
|
+
const rel = path.relative(repoRoot, dir).split(path.sep).join('/');
|
|
728
|
+
found.push(rel === '.' || rel === '' ? '' : rel);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return found;
|
|
732
|
+
}
|
|
553
733
|
/**
|
|
554
734
|
* Normalize a `.path` value from build.zig.zon into a repo-relative form.
|
|
555
735
|
* Returns null for paths that escape the repo root (start with `..`) or
|
|
@@ -557,13 +737,37 @@ export async function loadZigBuildConfig(repoRoot) {
|
|
|
557
737
|
* to the empty string (the repo root itself). Shared with the import
|
|
558
738
|
* resolver so both sides agree on which deps are in-repo.
|
|
559
739
|
*/
|
|
740
|
+
/**
|
|
741
|
+
* Does this `.path` value point outside the repository BY ITS SPELLING —
|
|
742
|
+
* POSIX absolute (`/dep`), Windows drive-qualified (`C:\dep`, `C:/dep`),
|
|
743
|
+
* root-relative (`\dep`) or UNC (`\\server\share`)?
|
|
744
|
+
*
|
|
745
|
+
* Separators are normalized first so every Windows spelling is visible to the
|
|
746
|
+
* one test. Exported-in-spirit rather than inlined because it must be asked in
|
|
747
|
+
* TWO places and the two must not drift: `normalizeZigDepPath` asks it of the
|
|
748
|
+
* value it is given, and `loadZigBuildConfig` asks it of a NESTED package's
|
|
749
|
+
* value BEFORE prefixing the package directory. That second call is the whole
|
|
750
|
+
* point — prefixing turns `/dep` into `packages/app//dep`, which is relative by
|
|
751
|
+
* inspection, so the check inside `normalizeZigDepPath` no longer sees an
|
|
752
|
+
* absolute path and the empty segment is simply dropped, mapping an
|
|
753
|
+
* out-of-repo dependency onto a real in-repo directory if one happens to exist.
|
|
754
|
+
*
|
|
755
|
+
* `path.posix.join` is NOT a substitute: it strips the leading slash too
|
|
756
|
+
* (`join('packages/app/', '/dep')` is `packages/app/dep`), so it produces the
|
|
757
|
+
* same fabricated path without ever rejecting anything.
|
|
758
|
+
*
|
|
759
|
+
* A `..` prefix is deliberately NOT handled here. `../core` escapes the
|
|
760
|
+
* package but not necessarily the repo, and rebasing it is exactly what the
|
|
761
|
+
* nested-package branch exists to do; `normalizeZigDepPath` rejects the ones
|
|
762
|
+
* that still escape the ROOT after rebasing.
|
|
763
|
+
*/
|
|
764
|
+
function isAbsoluteZigDepPath(depPath) {
|
|
765
|
+
const normalized = depPath.replace(/\\/g, '/');
|
|
766
|
+
return normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized);
|
|
767
|
+
}
|
|
560
768
|
export function normalizeZigDepPath(depPath) {
|
|
561
|
-
// Normalize separators BEFORE the absolute check so every Windows spelling
|
|
562
|
-
// is visible to it: POSIX (`/x`), drive (`C:\x`, `C:/x`), root-relative
|
|
563
|
-
// (`\x` → `/x`) and UNC (`\\server\share` → `//server/share`) paths all
|
|
564
|
-
// point outside the repository.
|
|
565
769
|
const normalized = depPath.replace(/\\/g, '/');
|
|
566
|
-
if (
|
|
770
|
+
if (isAbsoluteZigDepPath(depPath))
|
|
567
771
|
return null;
|
|
568
772
|
const parts = [];
|
|
569
773
|
for (const part of normalized.split('/')) {
|
|
@@ -51,6 +51,37 @@
|
|
|
51
51
|
*/
|
|
52
52
|
import Parser from 'tree-sitter';
|
|
53
53
|
export declare const TYPESCRIPT_SCOPE_QUERY: string;
|
|
54
|
+
/**
|
|
55
|
+
* JSX-only query suffix. Appended to the base query when compiling
|
|
56
|
+
* against the TSX grammar; NOT compiled against the plain TS grammar
|
|
57
|
+
* (which has no \`jsx_*\` node types and would reject these patterns).
|
|
58
|
+
*
|
|
59
|
+
* Why JSX as a CALLS edge: \`<Foo />\` is syntactic sugar for \`Foo(props)\`
|
|
60
|
+
* and the React component is invoked by the renderer, so for blast-radius
|
|
61
|
+
* (\`gitnexus_impact("Badge", direction: "upstream")\`) and call-graph
|
|
62
|
+
* (\`gitnexus_context("Foo")\`) purposes JSX usage IS a call. Routing
|
|
63
|
+
* through \`@reference.call.free\` / \`@reference.call.member\` makes the
|
|
64
|
+
* downstream caller-walk + edge-emission paths handle JSX uniformly with
|
|
65
|
+
* ordinary call expressions — no new edge type, no schema changes.
|
|
66
|
+
*
|
|
67
|
+
* Identifier-only JSX is filtered to PascalCase via \`(#match? ... "^[A-Z]")\`
|
|
68
|
+
* so \`<div>\`, \`<span>\`, \`<button>\` and other native HTML elements (which
|
|
69
|
+
* by JSX convention start lowercase) don't emit edges to nonexistent
|
|
70
|
+
* "div" / "span" symbols. Member-form JSX (\`<Foo.Bar />\`) is always a
|
|
71
|
+
* component (HTML element names can't contain dots), so no predicate
|
|
72
|
+
* filter is applied there.
|
|
73
|
+
*
|
|
74
|
+
* Both \`jsx_self_closing_element\` (\`<Foo />\`) and \`jsx_opening_element\`
|
|
75
|
+
* (\`<Foo>...</Foo>\`) emit; the closing tag is intentionally NOT captured —
|
|
76
|
+
* each JSX element should emit exactly one CALLS edge per use site.
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* Exported alongside `TYPESCRIPT_SCOPE_QUERY` so
|
|
80
|
+
* `value-ref-dispatchability.test.ts` checks the whole query a `.tsx` file is
|
|
81
|
+
* analyzed with. Checking the base alone would miss a `value-ref` rule added
|
|
82
|
+
* here. Not part of the provider surface — use `getTsScopeQuery`.
|
|
83
|
+
*/
|
|
84
|
+
export declare const TSX_JSX_QUERY_SUFFIX = "\n;; <Foo />\n((jsx_self_closing_element\n name: (identifier) @reference.name) @reference.call.free\n (#match? @reference.name \"^[A-Z]\"))\n\n;; <Foo> ... </Foo> (paired form \u2014 match the opening tag only)\n((jsx_opening_element\n name: (identifier) @reference.name) @reference.call.free\n (#match? @reference.name \"^[A-Z]\"))\n\n;; <Foo.Bar /> / <Container.Section.Title /> \u2014 namespaced JSX\n(jsx_self_closing_element\n name: (member_expression\n object: (_) @reference.receiver\n property: (property_identifier) @reference.name)) @reference.call.member\n\n(jsx_opening_element\n name: (member_expression\n object: (_) @reference.receiver\n property: (property_identifier) @reference.name)) @reference.call.member\n";
|
|
54
85
|
/**
|
|
55
86
|
* Return the right tree-sitter parser for `filePath` (or the TS parser
|
|
56
87
|
* when no path is given — the legacy callsite shape).
|
|
@@ -1338,7 +1338,13 @@ export const TYPESCRIPT_SCOPE_QUERY = `
|
|
|
1338
1338
|
* (\`<Foo>...</Foo>\`) emit; the closing tag is intentionally NOT captured —
|
|
1339
1339
|
* each JSX element should emit exactly one CALLS edge per use site.
|
|
1340
1340
|
*/
|
|
1341
|
-
|
|
1341
|
+
/**
|
|
1342
|
+
* Exported alongside `TYPESCRIPT_SCOPE_QUERY` so
|
|
1343
|
+
* `value-ref-dispatchability.test.ts` checks the whole query a `.tsx` file is
|
|
1344
|
+
* analyzed with. Checking the base alone would miss a `value-ref` rule added
|
|
1345
|
+
* here. Not part of the provider surface — use `getTsScopeQuery`.
|
|
1346
|
+
*/
|
|
1347
|
+
export const TSX_JSX_QUERY_SUFFIX = `
|
|
1342
1348
|
;; <Foo />
|
|
1343
1349
|
((jsx_self_closing_element
|
|
1344
1350
|
name: (identifier) @reference.name) @reference.call.free
|
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
+
/**
|
|
3
|
+
* Zig scope-resolution query (RFC #909 Ring 3).
|
|
4
|
+
*
|
|
5
|
+
* The grammar is vendored (`vendor/tree-sitter-zig`) and may be absent on a
|
|
6
|
+
* platform without a prebuild, so the language module is required lazily and
|
|
7
|
+
* `getZigParser` / `getZigScopeQuery` throw only when actually invoked without
|
|
8
|
+
* the grammar installed. That is safe: the parse pipeline filters `.zig` files
|
|
9
|
+
* through `parser-loader.isLanguageAvailable` before any scope extraction runs.
|
|
10
|
+
*
|
|
11
|
+
* Zig specifics encoded here:
|
|
12
|
+
* - Containers (struct/enum/union/opaque) are anonymous nodes bound by the
|
|
13
|
+
* enclosing `variable_declaration`; declarations capture the binding
|
|
14
|
+
* identifier from the wrapper.
|
|
15
|
+
* - `@import` is a builtin call, not import-statement syntax; the
|
|
16
|
+
* `#eq?` predicate keeps other builtins (@sizeOf, @as, …) out.
|
|
17
|
+
* - A plain `(variable_declaration (identifier))` rule would also match
|
|
18
|
+
* container and import bindings — `emitZigScopeCaptures` filters those
|
|
19
|
+
* groups out so a name binds exactly once.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Exported for `value-ref-dispatchability.test.ts`, which reads every language's
|
|
23
|
+
* scope query to enforce the keyed/unkeyed partition that
|
|
24
|
+
* `callableValueReferenceBoundaries`' dispatch exclusion depends on. Not part of
|
|
25
|
+
* the provider surface — nothing else should import it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ZIG_SCOPE_QUERY = "\n;; Scopes\n(source_file) @scope.module\n(struct_declaration) @scope.class\n(enum_declaration) @scope.class\n(union_declaration) @scope.class\n(opaque_declaration) @scope.class\n(function_declaration) @scope.function\n(test_declaration) @scope.function\n(block) @scope.block\n\n;; Declarations \u2014 functions (relabeled @declaration.method inside containers\n;; by emitZigScopeCaptures, mirroring the provider's labelOverride)\n(function_declaration\n name: (identifier) @declaration.name) @declaration.function\n\n;; Declarations \u2014 named tests. Same naming rule as ZIG_QUERIES: the string\n;; node WITH quotes, so the def joins the graph node and never collides with\n;; a same-named fn. Anonymous / decl-form tests are scopes without a def.\n(test_declaration\n (string) @declaration.name) @declaration.function\n\n;; Declarations \u2014 containers. The binding name lives on the wrapper\n;; variable_declaration, but the ANCHOR is the container node itself so its\n;; range equals the @scope.class range: the extractor then attaches the def\n;; to the class scope (walkers.populateClassOwnedMembers expects the\n;; class-like def among the class scope's ownedDefs) and auto-hoists the\n;; name binding to the parent scope. Keyword-gated like the ordinary\n;; binding rules: a keyword-less `x = struct {\u2026};` is an assignment\n;; (tree-sitter-zig 1.1.2 reuses variable_declaration), not a container def.\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (struct_declaration) @declaration.struct)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (struct_declaration) @declaration.struct)\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (enum_declaration) @declaration.enum)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (enum_declaration) @declaration.enum)\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (union_declaration) @declaration.union)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (union_declaration) @declaration.union)\n;; opaque {} is a fieldless container that may own methods \u2014 Struct, as in\n;; ZIG_QUERIES (see the rationale there).\n(variable_declaration\n \"const\" . (identifier) @declaration.name\n (opaque_declaration) @declaration.struct)\n(variable_declaration\n \"var\" . (identifier) @declaration.name\n (opaque_declaration) @declaration.struct)\n\n;; Declarations \u2014 generic type constructors. `fn List(comptime T: type) type\n;; { return struct {\u2026}; }` is Zig's only spelling of a generic type; the\n;; returned container is anonymous in the grammar but every reader (and every\n;; caller: `List(u8)`) names it after the function. Anchor on the container\n;; so the def sits in its own class scope; the name binding is hoisted to the\n;; MODULE scope by `zigBindingScopeFor` (not the fn body, where the name\n;; would be invisible to callers) and coexists with the Function def of the\n;; same name \u2014 `List` really is both a callable and a type.\n((function_declaration\n name: (identifier) @declaration.name\n type: (builtin_type) @_ret\n body: (block (expression_statement (return_expression\n (struct_declaration) @declaration.struct))))\n (#eq? @_ret \"type\"))\n((function_declaration\n name: (identifier) @declaration.name\n type: (builtin_type) @_ret\n body: (block (expression_statement (return_expression\n (union_declaration) @declaration.union))))\n (#eq? @_ret \"type\"))\n((function_declaration\n name: (identifier) @declaration.name\n type: (builtin_type) @_ret\n body: (block (expression_statement (return_expression\n (enum_declaration) @declaration.enum))))\n (#eq? @_ret \"type\"))\n\n;; Declarations \u2014 container fields (struct fields, enum/union variants).\n;; The #not-eq? guard drops the MISSING placeholder identifier tree-sitter-zig\n;; recovers for an empty container body (see ZIG_QUERIES). The optional\n;; `type:` (absent on enum variants) is captured as @declaration.field-type:\n;; `emitZigScopeCaptures` turns it into a @type-binding.field on the\n;; container's Class scope so `self.session.name()` can walk the field's\n;; type (Rust/Go parity \u2014 see the F5 block in captures.ts).\n((container_field\n name: (identifier) @declaration.name\n type: (_)? @declaration.field-type) @declaration.field\n (#not-eq? @declaration.name \"\"))\n\n;; Declarations \u2014 const/var bindings (import/container groups filtered in TS).\n;; The `.` anchor pins the FIRST named child: without it the pattern also\n;; matched the initializer of `const first = target;`, minting a phantom\n;; local named `target` that shadowed the real callee for every later\n;; reference in the block. The literal keyword is load-bearing too:\n;; tree-sitter-zig 1.1.2 parses statement assignments (`x = 5;`, `x += 1;`,\n;; `_ = expr;`) as `variable_declaration` WITHOUT a keyword child, and\n;; without the keyword every assignment and every discard minted a phantom\n;; local (one `_` per statement).\n(variable_declaration\n \"const\" . (identifier) @declaration.name) @declaration.variable\n(variable_declaration\n \"var\" . (identifier) @declaration.name) @declaration.variable\n\n;; Imports \u2014 const x = @import(\"...\") / var x = @import(\"...\"). Keyword-gated\n;; like every binding rule: a keyword-less `x = @import(\"...\")` is a\n;; statement (see the side-effect rule below), not a binding.\n(variable_declaration\n \"const\" . (identifier) @import.name\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.statement\n(variable_declaration\n \"var\" . (identifier) @import.name\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.statement\n\n;; Imports \u2014 const X = @import(\"...\").X : a NAMED import of one member. The\n;; local name is whatever the user chose (`const Alloc = @import(\"std\").mem;`\n;; is a rename), the imported name is the member. A deeper chain\n;; (`@import(\"std\").mem.Allocator`, `@import(\"lib.zig\").B.work`) is matched\n;; by the second rule below but is NOT bound as a named import of the\n;; innermost member: that discarded the written owner (`B`) and let a\n;; same-named `A.work` answer first. `emitZigScopeCaptures` binds the module\n;; under the builtin's text instead and rewrites the alias's use sites to the\n;; full path (`collectZigDeepAliases`, PR #1432 review 8.4).\n(variable_declaration\n \"const\" . (identifier) @import.name\n (field_expression\n object: (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n member: (identifier) @import.imported)\n (#eq? @_builtin \"@import\")) @import.statement\n(variable_declaration\n \"const\" . (identifier) @import.name\n (field_expression\n object: (field_expression\n object: (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source)))\n member: (identifier) @import.imported)\n (#eq? @_builtin \"@import\")) @import.statement\n\n;; Imports \u2014 a keyword-less `<ident> = @import(\"...\");` statement\n;; (`_ = @import(\"all_tests.zig\");` in a test block, the refAllDecls\n;; idiom): tree-sitter-zig reuses `variable_declaration` for assignments, so\n;; the shape is a declaration minus the keyword. It references the file\n;; without binding a name \u2014 a side-effect import. Tree-sitter queries cannot\n;; say \"no keyword child\", so this rule matches the keyword-bearing shapes\n;; too; `emitZigScopeCaptures` keeps it only when `isZigKeywordDeclaration`\n;; is false (the keyword shapes are the binding rules above).\n(variable_declaration\n . (identifier)\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.side-effect\n\n;; Aliases of a namespace member \u2014 const Counter = counter.Counter; where\n;; `counter` is an @import binding of THIS file. The query cannot know which\n;; identifiers are import bindings, so it captures every one-level member\n;; alias and `emitZigScopeCaptures` promotes the ones whose object is a\n;; known @import to a named import (same fact as `const Counter =\n;; @import(\"counter.zig\").Counter;`); the rest stay ordinary variables.\n;; Only the ONE-level shape is promoted; a deeper chain (`lib.B.work`,\n;; `std.mem.Allocator`) is a deep alias \u2014 a Const whose use sites are\n;; rewritten to the written owner path (see the import rule above, 8.4).\n(variable_declaration\n \"const\" . (identifier) @alias.name\n (field_expression\n object: (identifier) @alias.namespace\n member: (identifier) @alias.member) .) @alias.statement\n(variable_declaration\n \"const\" . (identifier) @alias.name\n (field_expression\n object: (field_expression\n object: (identifier) @alias.namespace)\n member: (identifier) @alias.member) .) @alias.statement\n\n;; Imports \u2014 pub usingnamespace @import(\"...\"); : every pub decl of the target\n;; becomes a decl of this container (removed from the language in 0.15, still\n;; everywhere in 0.11\u20130.14 code). Modelled as a wildcard import.\n(using_namespace_declaration\n (builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.wildcard\n\n;; Imports \u2014 `@import(\"...\")` in ANY other position: a tuple element\n;; (`pub const Interfaces = .{ @import(\"a.zig\"), @import(\"b.zig\") }`, the\n;; JS-API registration table), a call argument (`event.is(@import(\"x.zig\"))`),\n;; a comparison operand (`T == @import(\"x.zig\").T`), the receiver of a\n;; member call (`try @import(\"dump.zig\").root(...)`), a 3-deep member chain\u2026\n;; Every one of them is a file dependency; only the const/var/usingnamespace\n;; shapes above bind a name. This rule matches EVERY `@import` builtin, the\n;; bound shapes included \u2014 `emitZigScopeCaptures` drops the matches whose\n;; string node a binding rule (or the keyword-less side-effect rule) already\n;; claimed, so a bound import is never doubled, and emits the rest as\n;; side-effect imports (file edge, no binding) \u2014 except the member-call\n;; receiver, which becomes a namespace import keyed by its own source text so\n;; the call resolves into the imported module (see the emitter).\n((builtin_function\n (builtin_identifier) @_builtin\n (arguments (string) @import.source))\n (#eq? @_builtin \"@import\")) @import.inline\n\n;; Type bindings \u2014 parameter annotations (incl. self: *T receivers)\n(parameter\n name: (identifier) @type-binding.name\n type: (_) @type-binding.type) @type-binding.parameter\n\n;; Type bindings \u2014 constructor inference: const p = T{ ... }. Keyword-gated\n;; like every binding rule: a keyword-less `p = T{ ... };` is a\n;; re-assignment (same node type in tree-sitter-zig 1.1.2), and Zig's static\n;; typing means `p` already carries its type from its declaration\n;; (annotation, constructor or inferred value) \u2014 the assignment declares\n;; nothing, and `_ = T{ ... };` must not bind `_`.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (struct_initializer\n (identifier) @type-binding.type)) @type-binding.constructor\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (struct_initializer\n (identifier) @type-binding.type)) @type-binding.constructor\n\n;; Type bindings \u2014 qualified constructor: const p = mod.T{ ... }. The whole\n;; field_expression is captured so the dotted text \"mod.T\" survives \u2014\n;; receiver dispatch resolves the namespace prefix through the import\n;; binding (emitReceiverBoundCalls Case 3).\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (struct_initializer\n (field_expression) @type-binding.type)) @type-binding.constructor\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (struct_initializer\n (field_expression) @type-binding.type)) @type-binding.constructor\n\n;; Type bindings \u2014 generic instantiation literal: const l = List(u8){ ... }.\n;; The callee is the type constructor; `normalizeZigTypeName` drops the\n;; comptime argument list so `List(u8)` looks up `List`.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (struct_initializer\n (call_expression) @type-binding.type)) @type-binding.constructor\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (struct_initializer\n (call_expression) @type-binding.type)) @type-binding.constructor\n\n;; Type bindings \u2014 declared type: var x: T = \u2026; const x: T = .init(\u2026);\n;; The annotation is the ONLY type source for `= undefined` and for 0.14+\n;; decl literals (`.init`, `.empty`), which are the idiomatic\n;; constructors in current std. Ranked below constructor inference by the\n;; shared resolver (source 'annotation'), so a literal on the right still\n;; wins when both are present.\n(variable_declaration\n . (identifier) @type-binding.name\n type: (_) @type-binding.type) @type-binding.annotation\n\n;; Type bindings \u2014 value inference (F6): const t = <value>; where <value> is a\n;; call (`Counter.init()`, `makeThing()`, `node.asElement()`), possibly\n;; wrapped in `try` / `catch \u2026` / `\u2026 orelse \u2026` / parentheses \u2014 the shape of\n;; nearly every Zig constructor call (`const p = try Page.init(\u2026)`). The\n;; query only pins the declaration; `emitZigScopeCaptures` unwraps the\n;; wrappers and rewrites `@type-binding.type` to the type source (see\n;; `zigCallReturnTypeOf`). Keyword-gated: `_ = e.top();` is an assignment\n;; (same node type), not a binding of `_`. Rust's twin is\n;; `let x = Foo::new()` / `let x = foo().await`.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (_) @type-binding.value .) @type-binding.call-return\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (_) @type-binding.value .) @type-binding.call-return\n\n;; Type bindings \u2014 return-type annotation (F6): `fn make() !*Thing` binds\n;; `make \u21A6 Thing` in the enclosing scope (Module for free fns, the container's\n;; Class scope for methods \u2014 that is where the compound resolver reads a\n;; method's return type for `node.asElement()`), so `const t = makeThing()`\n;; chains to `Thing`. Rust: `(function_item \u2026 return_type:) @type-binding.return`.\n;; `emitZigScopeCaptures` drops builtin / `type` returns (a `List \u21A6 type`\n;; binding would hijack the `List(u8){}` constructor chain).\n(function_declaration\n name: (identifier) @type-binding.name\n type: (_) @type-binding.type) @type-binding.return\n\n;; Type bindings \u2014 aliases (F5 field-access aliases + F7 type aliases):\n;; `const page = self.page;` (F5: the RHS path is kept verbatim as the\n;; \"type\", the compound resolver's member-alias branch re-resolves it as a\n;; receiver chain \u2014 head `self` \u2192 class \u2192 field type), `const LocalAlias = Local;`,\n;; `const Proto = HtmlElement;`, `const T2 = Thing;` (alias of an alias /\n;; import), `const B = util.List(u8);` (an INSTANTIATED generic type\n;; constructor). Zig has no `type X = Y` syntax \u2014 a type alias is a const\n;; whose value is a type expression, and it stays a Const in the graph. What\n;; must change is the scope side: bind the alias NAME to the value's type\n;; text (Rust's `let x = y` / JS's `const B = Foo` `@type-binding.alias`,\n;; source 'assignment-inferred'), so `LocalAlias.mk()` types through Case 4,\n;; `B.init()` / `x: B` / `B{}` through Case 3 once `normalizeZigTypeName`\n;; drops the comptime arguments (`util.List(u8)` \u2192 `util.List`), and every\n;; binding that names the alias (`var l = LocalAlias.mk()`, `var x: B`) is\n;; chained to the target by the shared `followChainedRef` /\n;; `followChainPostFinalize`. The identifier / member shapes take `var` too:\n;; `var node = orig_node;` is the same value alias as Rust's `let x = y`\n;; and chains to the type of `orig_node` (a Zig type is comptime and never\n;; `var`, so the type-alias reading only ever applies to `const`). The\n;; call shape is `const`-only and kept only when the callee's last\n;; identifier is TitleCase \u2014 see `emitZigScopeCaptures` (a value call\n;; `const t = util.makeThing()` belongs to the call-return rules above and\n;; must not receive a competing binding). A promoted namespace-member alias\n;; (`const Counter = counter.Counter;` \u2192 named import) is skipped there too:\n;; the import binding already carries the type.\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (identifier) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (identifier) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (field_expression) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"var\" . (identifier) @type-binding.name\n (field_expression) @type-binding.type .) @type-binding.alias\n(variable_declaration\n \"const\" . (identifier) @type-binding.name\n (call_expression) @type-binding.type .) @type-binding.alias\n\n;; References \u2014 VALUE positions (#3399): a callable named where a value is\n;; expected rather than where a callee is. Zig's JS bridge is built entirely\n;; out of this shape \u2014\n;;\n;; pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{});\n;;\n;; \u2014 2,047 such declarations across 257 files in lightpanda-io/browser, the\n;; project's whole JS\u2194Zig surface, and NONE of them reached the graph: Zig\n;; emitted no `value-ref` capture at all, so a public DOM accessor's only\n;; recorded callers were the two internal ones and `impact` called that `exact`.\n;;\n;; These become reference-class USES edges through the existing\n;; `mapReferenceKindToEdgeType` mapping \u2014 a registration is not an invocation\n;; (Kythe `ref` vs `ref/call`; Joern `METHOD_REF`) \u2014 resolved by the\n;; property-dispatch pass, which keeps ONLY callable targets. That callable gate\n;; is what makes these deliberately broad rules safe: `js.Bridge(Element)` and\n;; `register(count)` match too, and emit nothing, exactly as TypeScript's\n;; `{ port: DEFAULT_PORT }` does.\n;;\n;; No `@reference.property-key` is attached: Zig has no object-literal key to\n;; dispatch through, so these register a reference and never synthesize CALLS.\n;; The terminal invoke \u2014 `Accessor.init` \u2192 a struct field \u2192 `Factory.zig`'s\n;; `inline for`/`@typeInfo` \u2192 `@call(.auto, func, args)` \u2014 needs comptime\n;; evaluation and is deliberately NOT modelled; `impact` reports the shortfall\n;; as `epistemic: \"lower-bound\"` instead of pretending to certainty.\n\n;; Call ARGUMENTS. In tree-sitter-zig arguments are direct children of\n;; `call_expression`, NOT wrapped in an `arguments` node (only builtins have\n;; one), so the callee has to be consumed explicitly by `function:` \u2014 without\n;; that binding the same rule also matches the callee of `foo(bar)` and mints a\n;; USES edge duplicating the call.\n(call_expression\n function: (_)\n (identifier) @reference.name @reference.value-ref)\n\n;; Qualified argument \u2014 `bridge.accessor(Element.getNamespaceUri, \u2026)`. The\n;; RECEIVER is captured alongside the member so the site carries the owner it\n;; was written with; `@reference.name` stays the member, which is the name the\n;; scope walk resolves.\n(call_expression\n function: (_)\n (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name) @reference.value-ref)\n\n;; Const binding initialiser \u2014 `pub const defaultHandler = onReset;`. Both\n;; anchors are load-bearing: the leading `.` pins the bound name to the first\n;; named child (see the declaration rules above), and the trailing `.` keeps the\n;; initializer as the LAST child, so a `const x: T = y` annotation shape cannot\n;; put the TYPE in value position.\n(variable_declaration\n \"const\" . (identifier)\n (identifier) @reference.name @reference.value-ref .)\n\n;; References \u2014 free calls: foo(...)\n(call_expression\n function: (identifier) @reference.name) @reference.call.free\n\n;; References \u2014 member calls: obj.method(...) / mod.fn(...)\n(call_expression\n function: (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name)) @reference.call.member\n\n;; References \u2014 constructor uses: T{ ... }\n(struct_initializer\n (identifier) @reference.name) @reference.call.constructor\n\n;; References \u2014 qualified constructor uses: mod.T{ ... } / hub.sub.T{ ... }\n;; Captured with the RECEIVER, not as a free constructor with a raw qualified\n;; name, on purpose: the free-call fallback resolves a qualified constructor by\n;; its simple tail, and a workspace-unique `Thing` then answers for\n;; `other.Thing{}` whichever module the source named (measured: `c.Thing{}`\n;; with no `Thing` in c.zig bound to a.zig's). With the receiver the site goes\n;; through the receiver-bound namespace case, which resolves the member inside\n;; the module the receiver is bound to \u2014 the same path `mod.fn()` takes \u2014 so\n;; `a.Thing{}` and `b.Thing{}` each bind their own file and `std.Thread.Mutex{}`\n;; binds nothing even when a local `Mutex` exists.\n(struct_initializer\n (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name)) @reference.call.constructor\n\n;; References \u2014 generic instantiation literals: List(u8){ ... } /\n;; lists.List(u8){ ... }. The type head is a call_expression \u2014 the\n;; instantiation of the type constructor \u2014 which neither constructor rule\n;; above matches, so the OUTER aggregate event had no site: only the inner\n;; `List(u8)` call (a free / member call reference on the call node) reached\n;; the graph (PR #1432 review, 8.11). Two sites on two anchors: the call\n;; (an invocation of `List`) and this initializer (a construction of the\n;; container `List` returns, marked `(constructor)`). The receiver form goes\n;; through the same namespace path as `mod.T{}`.\n(struct_initializer\n (call_expression\n function: (identifier) @reference.name)) @reference.call.constructor\n(struct_initializer\n (call_expression\n function: (field_expression\n object: (_) @reference.receiver\n member: (identifier) @reference.name))) @reference.call.constructor\n";
|
|
2
28
|
export declare function getZigParser(): Parser;
|
|
3
29
|
export declare function getZigScopeQuery(): Parser.Query;
|
|
@@ -19,7 +19,13 @@ import { requireVendoredGrammar } from '../../../tree-sitter/vendored-grammars.j
|
|
|
19
19
|
* container and import bindings — `emitZigScopeCaptures` filters those
|
|
20
20
|
* groups out so a name binds exactly once.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Exported for `value-ref-dispatchability.test.ts`, which reads every language's
|
|
24
|
+
* scope query to enforce the keyed/unkeyed partition that
|
|
25
|
+
* `callableValueReferenceBoundaries`' dispatch exclusion depends on. Not part of
|
|
26
|
+
* the provider surface — nothing else should import it.
|
|
27
|
+
*/
|
|
28
|
+
export const ZIG_SCOPE_QUERY = `
|
|
23
29
|
;; Scopes
|
|
24
30
|
(source_file) @scope.module
|
|
25
31
|
(struct_declaration) @scope.class
|
|
@@ -358,6 +364,60 @@ const ZIG_SCOPE_QUERY = `
|
|
|
358
364
|
"const" . (identifier) @type-binding.name
|
|
359
365
|
(call_expression) @type-binding.type .) @type-binding.alias
|
|
360
366
|
|
|
367
|
+
;; References — VALUE positions (#3399): a callable named where a value is
|
|
368
|
+
;; expected rather than where a callee is. Zig's JS bridge is built entirely
|
|
369
|
+
;; out of this shape —
|
|
370
|
+
;;
|
|
371
|
+
;; pub const namespaceURI = bridge.accessor(Element.getNamespaceUri, null, .{});
|
|
372
|
+
;;
|
|
373
|
+
;; — 2,047 such declarations across 257 files in lightpanda-io/browser, the
|
|
374
|
+
;; project's whole JS↔Zig surface, and NONE of them reached the graph: Zig
|
|
375
|
+
;; emitted no \`value-ref\` capture at all, so a public DOM accessor's only
|
|
376
|
+
;; recorded callers were the two internal ones and \`impact\` called that \`exact\`.
|
|
377
|
+
;;
|
|
378
|
+
;; These become reference-class USES edges through the existing
|
|
379
|
+
;; \`mapReferenceKindToEdgeType\` mapping — a registration is not an invocation
|
|
380
|
+
;; (Kythe \`ref\` vs \`ref/call\`; Joern \`METHOD_REF\`) — resolved by the
|
|
381
|
+
;; property-dispatch pass, which keeps ONLY callable targets. That callable gate
|
|
382
|
+
;; is what makes these deliberately broad rules safe: \`js.Bridge(Element)\` and
|
|
383
|
+
;; \`register(count)\` match too, and emit nothing, exactly as TypeScript's
|
|
384
|
+
;; \`{ port: DEFAULT_PORT }\` does.
|
|
385
|
+
;;
|
|
386
|
+
;; No \`@reference.property-key\` is attached: Zig has no object-literal key to
|
|
387
|
+
;; dispatch through, so these register a reference and never synthesize CALLS.
|
|
388
|
+
;; The terminal invoke — \`Accessor.init\` → a struct field → \`Factory.zig\`'s
|
|
389
|
+
;; \`inline for\`/\`@typeInfo\` → \`@call(.auto, func, args)\` — needs comptime
|
|
390
|
+
;; evaluation and is deliberately NOT modelled; \`impact\` reports the shortfall
|
|
391
|
+
;; as \`epistemic: "lower-bound"\` instead of pretending to certainty.
|
|
392
|
+
|
|
393
|
+
;; Call ARGUMENTS. In tree-sitter-zig arguments are direct children of
|
|
394
|
+
;; \`call_expression\`, NOT wrapped in an \`arguments\` node (only builtins have
|
|
395
|
+
;; one), so the callee has to be consumed explicitly by \`function:\` — without
|
|
396
|
+
;; that binding the same rule also matches the callee of \`foo(bar)\` and mints a
|
|
397
|
+
;; USES edge duplicating the call.
|
|
398
|
+
(call_expression
|
|
399
|
+
function: (_)
|
|
400
|
+
(identifier) @reference.name @reference.value-ref)
|
|
401
|
+
|
|
402
|
+
;; Qualified argument — \`bridge.accessor(Element.getNamespaceUri, …)\`. The
|
|
403
|
+
;; RECEIVER is captured alongside the member so the site carries the owner it
|
|
404
|
+
;; was written with; \`@reference.name\` stays the member, which is the name the
|
|
405
|
+
;; scope walk resolves.
|
|
406
|
+
(call_expression
|
|
407
|
+
function: (_)
|
|
408
|
+
(field_expression
|
|
409
|
+
object: (_) @reference.receiver
|
|
410
|
+
member: (identifier) @reference.name) @reference.value-ref)
|
|
411
|
+
|
|
412
|
+
;; Const binding initialiser — \`pub const defaultHandler = onReset;\`. Both
|
|
413
|
+
;; anchors are load-bearing: the leading \`.\` pins the bound name to the first
|
|
414
|
+
;; named child (see the declaration rules above), and the trailing \`.\` keeps the
|
|
415
|
+
;; initializer as the LAST child, so a \`const x: T = y\` annotation shape cannot
|
|
416
|
+
;; put the TYPE in value position.
|
|
417
|
+
(variable_declaration
|
|
418
|
+
"const" . (identifier)
|
|
419
|
+
(identifier) @reference.name @reference.value-ref .)
|
|
420
|
+
|
|
361
421
|
;; References — free calls: foo(...)
|
|
362
422
|
(call_expression
|
|
363
423
|
function: (identifier) @reference.name) @reference.call.free
|