mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
Merge branch 'main' into fix/skill-evolution-gate
This commit is contained in:
commit
1b6c1f9d69
158 changed files with 12905 additions and 158 deletions
12
.github/workflows/ci-tests.yml
vendored
12
.github/workflows/ci-tests.yml
vendored
|
|
@ -24,8 +24,14 @@ jobs:
|
|||
shard: ${{ fromJSON(needs.shard-plan.outputs.cov_shards) }}
|
||||
# Fail loudly (don't silently skip) if the FTS extension is unavailable, so
|
||||
# FTS-dependent lbug integration suites are guaranteed to run in CI.
|
||||
# Same contract for Zig's optionalDependency grammar: this runner is
|
||||
# linux-x64, which @tree-sitter-grammars/tree-sitter-zig publishes a
|
||||
# prebuild for, so an absent grammar here is a packaging regression and not
|
||||
# an unsupported platform. Without it every Zig suite skips and the job is
|
||||
# green having never executed the native Zig parser once.
|
||||
env:
|
||||
GITNEXUS_REQUIRE_FTS: '1'
|
||||
GITNEXUS_REQUIRE_ZIG: '1'
|
||||
steps:
|
||||
# persist-credentials: false — runs tests + uploads a blob artifact; the
|
||||
# default-persisted token must not be capturable through it (zizmor
|
||||
|
|
@ -278,8 +284,14 @@ jobs:
|
|||
shell: bash
|
||||
run: python3 .github/scripts/check-tree-sitter-upgrade-readiness.py --assert-current
|
||||
|
||||
# GITNEXUS_REQUIRE_ZIG=1: every OS in this matrix has a published
|
||||
# tree-sitter-zig prebuild, so the smoke's "optional grammar may be
|
||||
# absent" exemption is revoked here and an ABI-broken Zig binding fails
|
||||
# the job instead of being accepted as a clean absence.
|
||||
- name: Run parser-loader ABI load-smoke (dynamic)
|
||||
run: npx vitest run test/unit/parser-loader-abi.test.ts
|
||||
env:
|
||||
GITNEXUS_REQUIRE_ZIG: '1'
|
||||
working-directory: gitnexus
|
||||
|
||||
# End-to-end smoke test for the #1728 packaging fix: pack the published
|
||||
|
|
|
|||
|
|
@ -657,6 +657,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
|
|||
| C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ |
|
||||
| C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
|
||||
| Dart | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
|
||||
| Zig | ✓ | — | ✓ | — | ✓ | ✓ | ✓ | — | ✓ |
|
||||
|
||||
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
|
|||
[SupportedLanguages.Dart]: ['.dart'],
|
||||
[SupportedLanguages.Vue]: ['.vue'],
|
||||
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
|
||||
[SupportedLanguages.Zig]: ['.zig'],
|
||||
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
|
||||
|
||||
/** Pre-built reverse lookup: extension → language (built once at module load). */
|
||||
|
|
@ -121,6 +122,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
|
|||
[SupportedLanguages.Dart]: 'dart',
|
||||
[SupportedLanguages.Vue]: 'typescript',
|
||||
[SupportedLanguages.Cobol]: 'cobol',
|
||||
[SupportedLanguages.Zig]: 'zig',
|
||||
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness
|
||||
|
||||
/** Non-code file extensions → Prism-compatible syntax identifiers */
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export enum SupportedLanguages {
|
|||
Vue = 'vue',
|
||||
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
|
||||
Cobol = 'cobol',
|
||||
Zig = 'zig',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1072,6 +1072,7 @@ const CALLABLE_OR_TYPE_LIKE: ReadonlySet<string> = new Set([
|
|||
'Interface',
|
||||
'Enum',
|
||||
'Struct',
|
||||
'Union',
|
||||
'Record',
|
||||
'Trait',
|
||||
'Namespace',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
* - experimental: vue (embedded-language / SFC complexity),
|
||||
* cobol (regex-provider path)
|
||||
* - quarantined: (none)
|
||||
*
|
||||
* Added after Ring 1: zig enters as `experimental` (new language
|
||||
* integration; promotion to `production` is a separate governance PR).
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from '../languages.js';
|
||||
|
|
@ -41,6 +44,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
|
|||
[SupportedLanguages.Dart]: 'production',
|
||||
[SupportedLanguages.Vue]: 'experimental',
|
||||
[SupportedLanguages.Cobol]: 'experimental',
|
||||
[SupportedLanguages.Zig]: 'experimental',
|
||||
};
|
||||
|
||||
/** Convenience predicate: is this language gating Ring 4 retirement? */
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@
|
|||
|
||||
import type { NodeLabel } from '../graph/types.js';
|
||||
import type { SymbolDefinition } from './symbol-definition.js';
|
||||
// Type-only, so the `reference-site.ts` → `types.ts` import cycle is erased
|
||||
// at compile time.
|
||||
import type { CallForm } from './reference-site.js';
|
||||
|
||||
// ─── §2.1 Type aliases ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -752,6 +755,15 @@ export interface Reference {
|
|||
| 'import-use'
|
||||
| 'value-ref'
|
||||
| 'macro';
|
||||
/**
|
||||
* Call form of the site this reference was resolved from, copied verbatim
|
||||
* from `ReferenceSite.callForm`; set only when `kind === 'call'`. The
|
||||
* emit phase reads it to tell a construction site (`T{…}`, `new T()`,
|
||||
* `T { .. }` — form `'constructor'`) apart from an invocation, which in the
|
||||
* graph are both `CALLS` edges. Optional and additive: a `Reference` built
|
||||
* without it is emitted exactly as before.
|
||||
*/
|
||||
readonly callForm?: CallForm;
|
||||
readonly confidence: number;
|
||||
readonly evidence: readonly ResolutionEvidence[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ GitNexus supports indexing multiple repositories. Each `gitnexus analyze` regist
|
|||
|
||||
## Supported Languages
|
||||
|
||||
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart
|
||||
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart, Zig
|
||||
|
||||
### Language Feature Matrix
|
||||
|
||||
|
|
@ -436,6 +436,7 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
|
|||
| C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ |
|
||||
| C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
|
||||
| Dart | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
|
||||
| Zig | ✓ | — | ✓ | — | ✓ | ✓ | ✓ | — | ✓ |
|
||||
|
||||
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
|
||||
|
||||
|
|
@ -594,7 +595,7 @@ results until you re-run `gitnexus analyze --repair-fts` from a shell where the
|
|||
|
||||
### Installation fails with native module errors
|
||||
|
||||
Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing.
|
||||
Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing. Zig (`@tree-sitter-grammars/tree-sitter-zig`, an npm `optionalDependency`) behaves the same way: if its native binding fails to install, `.zig` files are skipped.
|
||||
|
||||
If `npm install -g gitnexus` fails on native modules:
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,9 @@
|
|||
* `--check` inventory arm at the foot of this file fails when the two disagree
|
||||
* — over ONE shared corpus so the arms are directly comparable. One arm per
|
||||
* registered language, plus a second `csharp` arm carrying csproj configs
|
||||
* (#2902), so there is one more arm than there are languages.
|
||||
* (#2902), so there is one more arm than there are languages. The newest row
|
||||
* is `zig` (PR #1432), added the day its resolver registered — the inventory
|
||||
* arm below is what noticed it missing, which is the arm doing its job.
|
||||
*
|
||||
* NO LANGUAGE IS OMITTED, and that is the point of the list rather than an
|
||||
* accident of it. Nine of these arms (go, csharp, csharp_csproj, dart, ruby,
|
||||
|
|
@ -109,6 +111,19 @@
|
|||
* depth-then-lexicographic tie-break, so the collide arm (a `mod{n}` header
|
||||
* in every service's `include/`) is where it grows: 2.54 / 2.64 against
|
||||
* 1.06 on file count.
|
||||
* - zig: `resolveZigImportInternal` is rust's shape — an `@import("…zig")`
|
||||
* path is walked component by component from the importer's directory and
|
||||
* probed with two `allFiles.has(...)` calls (as written, then `+ '.zig'`),
|
||||
* and a bare name is a Map lookup in the build config or a miss. No index
|
||||
* is built, so the cost is O(path SEGMENTS) and flat in the file count,
|
||||
* and — as for rust — its collide arm is a deep tree whose spellings carry
|
||||
* ~4x the components rather than a shared-leaf layout that cannot fail.
|
||||
* The arm passes NO build config (`buildZon` null): the bare-name legs
|
||||
* (`b.addModule` roots, zon `.path` deps) read `build.zig` / `build.zig.zon`
|
||||
* through `loadZigBuildConfig` and are gated by
|
||||
* `test/unit/zig-import-resolver.test.ts`, so this fingerprint pins the
|
||||
* path-walking resolver alone and does not move when that config parsing
|
||||
* changes.
|
||||
*
|
||||
* Two properties of the corpus are load-bearing and must not be "simplified":
|
||||
*
|
||||
|
|
@ -221,7 +236,9 @@
|
|||
* corpus is a deep module tree whose targets carry ~2x the `::` segments,
|
||||
* which is the axis that CAN grow; the ratio across file counts staying at
|
||||
* 1.06 on it is the assertion, and `collide_ms_ceiling` bounds the absolute
|
||||
* cost of the long-path probe.
|
||||
* cost of the long-path probe. zig's collide arm is built the same way and
|
||||
* for the same reason: a deep tree whose `../../…/l4/mod{n}/file.zig`
|
||||
* spellings walk ~4x the components of the unique arm's `../mod{n}/…`.
|
||||
*
|
||||
* This is a scope-of-claim limit, not a regression: on the MISS path with a
|
||||
* shared leaf name the bucket grows with the file count BY CONSTRUCTION, and
|
||||
|
|
@ -308,7 +325,9 @@
|
|||
*
|
||||
* Only rust's exclusion survived unchanged: 16 B at 8000 files and 16 B at
|
||||
* 32 000, identical in all five runs, because it probes candidate paths with
|
||||
* `allFilePaths.has(...)` and builds nothing.
|
||||
* `allFilePaths.has(...)` and builds nothing. zig joined that tier on the same
|
||||
* reading for the same reason (`resolveZigImportInternal` holds no per-pass
|
||||
* structure at all), and takes rust's absolute 1 MiB bound.
|
||||
*
|
||||
* So the nine are still not BUDGETED — their ceilings, floors and ratio arms
|
||||
* are not this change to write — but they are all measured and all bounded. See
|
||||
|
|
@ -463,6 +482,7 @@ import { javaScopeResolver } from '../../src/core/ingestion/languages/java/scope
|
|||
import { cobolScopeResolver } from '../../src/core/ingestion/languages/cobol/scope-resolver.ts';
|
||||
import { resolveSwiftImportTarget } from '../../src/core/ingestion/languages/swift/import-target.ts';
|
||||
import { resolveRustImportTarget } from '../../src/core/ingestion/languages/rust/import-target.ts';
|
||||
import { resolveZigImportInternal } from '../../src/core/ingestion/import-resolvers/zig.ts';
|
||||
import { resolvePythonImportTarget } from '../../src/core/ingestion/languages/python/import-target.ts';
|
||||
import { makeJsResolveImportTarget } from '../../src/core/ingestion/languages/javascript/import-target.ts';
|
||||
import { makeVueResolveImportTarget } from '../../src/core/ingestion/languages/vue/import-target.ts';
|
||||
|
|
@ -736,6 +756,7 @@ const EXTENSION = {
|
|||
vue: '.vue',
|
||||
c: '.c',
|
||||
cpp: '.cpp',
|
||||
zig: '.zig',
|
||||
};
|
||||
/** C and C++ resolve `#include` against HEADERS, which reach the resolver
|
||||
* through `resolutionConfig` rather than through `allFilePaths` — see
|
||||
|
|
@ -859,6 +880,13 @@ function uniqueDir(lang, d, i) {
|
|||
// `resolutionConfig` load-bearing. Odd `i` is the header.
|
||||
if (lang === 'c' || lang === 'cpp') return i % 2 === 1 ? `include/comp${d}` : `src/comp${d}`;
|
||||
if (lang === 'ruby') return `lib/mod${d}`;
|
||||
// One flat `src/mod{d}/` per index and NO nested slice, on purpose: a Zig
|
||||
// import is spelled RELATIVE TO THE IMPORTER, and `uniqueTarget` does not
|
||||
// know which file issues it, so every importer has to sit at one depth for
|
||||
// `../mod{n}/file{j}.zig` to mean the same file from all of them. The miss
|
||||
// share the other unique arms take from a nested directory comes from the
|
||||
// target instead (see `uniqueTarget`).
|
||||
if (lang === 'zig') return `src/mod${d}`;
|
||||
throw unwiredLanguage('uniqueDir', lang);
|
||||
}
|
||||
|
||||
|
|
@ -947,6 +975,12 @@ function collideDir(lang, d, i) {
|
|||
if (lang === 'vue') return `src/pkg${d}/components`;
|
||||
if (lang === 'c' || lang === 'cpp') return i % 2 === 1 ? `svc${d}/include` : `svc${d}/src`;
|
||||
if (lang === 'ruby') return `svc${d}/lib/models`;
|
||||
// Rust's reasoning, verbatim: the resolver walks path components and probes
|
||||
// `.has()`, never searches, so file count is not an axis its cost has and a
|
||||
// shared-leaf layout would be an arm that cannot fail. A deep tree is the
|
||||
// axis that CAN grow — `collideTarget` spells its imports up through the
|
||||
// tree and back down, ~4x the components of the unique arm.
|
||||
if (lang === 'zig') return `src/l0/l1/l2/l3/l4/mod${d}`;
|
||||
throw unwiredLanguage('collideDir', lang);
|
||||
}
|
||||
|
||||
|
|
@ -1373,6 +1407,29 @@ function uniqueTarget(lang, { local, r, d, j, dirs }) {
|
|||
? ['json', 'set', 'net/http', 'digest'][(r >>> 4) % 4]
|
||||
: `gem${(r >>> 4) % 97}/missing/thing`;
|
||||
}
|
||||
if (lang === 'zig') {
|
||||
// `@import("../mod{n}/file{j}.zig")`, importer-relative — every file sits
|
||||
// in `src/mod{d}/`, so one `..` reaches `src/` from all of them (see
|
||||
// `uniqueDir`). The target is file `j`'s OWN directory, `j % dirs`, so a
|
||||
// hit is a real file; the `d % 7` slice names an `inner/` that exists
|
||||
// nowhere and misses, which is where the resolved count comes from, as in
|
||||
// the rust arm. One local spelling in three drops the extension, which is
|
||||
// the second `.has()` probe (`candidate + '.zig'`) — the leg an
|
||||
// extension-only corpus would never reach. The misses are the three
|
||||
// kinds a Zig file has: the compiler's own modules (`std`, `builtin`,
|
||||
// `root`), which the resolver rejects by name before any walk; a bare
|
||||
// package name with no build config to map it, which falls through every
|
||||
// leg to null; and a relative path to a vendored file that is not in the
|
||||
// corpus, which walks to the end and misses on both probes.
|
||||
if (local) {
|
||||
if (d % 7 === 0) return `../mod${d}/inner/file${j}.zig`;
|
||||
return (r >>> 3) % 3 === 0 ? `../mod${j % dirs}/file${j}` : `../mod${j % dirs}/file${j}.zig`;
|
||||
}
|
||||
const miss = (r >>> 3) % 3;
|
||||
if (miss === 0) return ['std', 'builtin', 'root'][(r >>> 4) % 3];
|
||||
if (miss === 1) return `ghost${(r >>> 4) % 97}`;
|
||||
return `../vendor${(r >>> 4) % 97}/missing.zig`;
|
||||
}
|
||||
throw unwiredLanguage('uniqueTarget', lang);
|
||||
}
|
||||
|
||||
|
|
@ -1583,6 +1640,27 @@ function collideTarget(lang, { local, r, d, j, dirs }) {
|
|||
? ['json', 'set', 'net/http', 'digest'][(r >>> 4) % 4]
|
||||
: `gem${(r >>> 4) % 97}/missing/thing`;
|
||||
}
|
||||
if (lang === 'zig') {
|
||||
// The same three families in the same proportions as the unique arm, so
|
||||
// the resolved count is identical by construction (asserted), spelled up
|
||||
// six levels to `src/` and back down through `l0/…/l4` — thirteen
|
||||
// components against the unique arm's three, in the hits and in the path
|
||||
// misses alike, because component count is the only axis this resolver's
|
||||
// cost has. The `d % 7` slice and the extension-less third mirror the
|
||||
// unique arm's; the by-name misses are unchanged, since no walk is what
|
||||
// they measure.
|
||||
const up = '../../../../../../l0/l1/l2/l3/l4';
|
||||
if (local) {
|
||||
if (d % 7 === 0) return `${up}/mod${d}/inner/file${j}.zig`;
|
||||
return (r >>> 3) % 3 === 0
|
||||
? `${up}/mod${j % dirs}/file${j}`
|
||||
: `${up}/mod${j % dirs}/file${j}.zig`;
|
||||
}
|
||||
const miss = (r >>> 3) % 3;
|
||||
if (miss === 0) return ['std', 'builtin', 'root'][(r >>> 4) % 3];
|
||||
if (miss === 1) return `ghost${(r >>> 4) % 97}`;
|
||||
return `${up}/vendor${(r >>> 4) % 97}/missing.zig`;
|
||||
}
|
||||
throw unwiredLanguage('collideTarget', lang);
|
||||
}
|
||||
|
||||
|
|
@ -1784,6 +1862,10 @@ function resolveOne(lang, from, target, pass) {
|
|||
);
|
||||
}
|
||||
if (lang === 'rust') return resolveRustImportTarget(target, from, allFilePaths, undefined);
|
||||
// Quotes already stripped — `configs/zig.ts` strips them before this call in
|
||||
// production too. `null` build config: this arm pins the path walk alone
|
||||
// (see the header); the config legs are gated by their own unit tests.
|
||||
if (lang === 'zig') return resolveZigImportInternal(from, target, allFilePaths, null);
|
||||
if (lang === 'python') {
|
||||
// `from <target> import X` — the spelling the orchestrator actually hands
|
||||
// the provider, and the ONLY one that reads `context.parsedFiles`: a
|
||||
|
|
@ -2062,7 +2144,7 @@ const HEAP_PROBE_TARGET = {
|
|||
// - `kotlin` misses after building its declared-package/module-binding index;
|
||||
// - `cobol` misses in both tier maps, `swift` in `byModule`, and `rust`
|
||||
// probes candidate paths and builds nothing — that last is the reading
|
||||
// the exclusion rests on;
|
||||
// the exclusion rests on, and `zig` shares it exactly;
|
||||
// - `typescript`, `vue` and `cpp` carry the same spelling shape as the
|
||||
// `javascript` and `c` arms they are excluded as duplicates OF, so the
|
||||
// bound compares like with like. `vue`'s is bare rather than `@/…`
|
||||
|
|
@ -2073,6 +2155,10 @@ const HEAP_PROBE_TARGET = {
|
|||
cobol: 'VENDOR0',
|
||||
swift: 'ExternalPkg0',
|
||||
rust: 'ghost0::Missing',
|
||||
// A relative path to a file the corpus does not hold: both `.has()` probes
|
||||
// miss after the full component walk, which is the longest leg the resolver
|
||||
// has (a by-name miss returns before any walk).
|
||||
zig: '../vendor0/missing.zig',
|
||||
typescript: 'vendor0/lib/missing',
|
||||
vue: 'vendor0/lib/Missing.vue',
|
||||
cpp: 'vendor0/missing.hpp',
|
||||
|
|
@ -2308,6 +2394,7 @@ const LANG_REGISTRY = {
|
|||
vue: SupportedLanguages.Vue,
|
||||
c: SupportedLanguages.C,
|
||||
cpp: SupportedLanguages.CPlusPlus,
|
||||
zig: SupportedLanguages.Zig,
|
||||
};
|
||||
const LANGS = Object.keys(LANG_REGISTRY);
|
||||
/**
|
||||
|
|
@ -2853,17 +2940,19 @@ for (const lang of HEAP_BUDGETED) {
|
|||
* `heap_bound_bytes` is the "exclusion still holds" bound. It does not claim
|
||||
* these indexes are small enough, which is what a ceiling claims about a
|
||||
* budgeted one; it claims each is still the SIZE the decision to leave it out
|
||||
* was taken on. `HEAP_BOUNDED` derives to THREE today — cobol, swift, rust.
|
||||
* was taken on. `HEAP_BOUNDED` derives to SEVEN today — cobol, swift, rust,
|
||||
* the ts family (#2953), and zig, which reads what rust reads (16 B) because
|
||||
* `resolveZigImportInternal` builds nothing and takes rust's absolute bound.
|
||||
* The prose below still counts nine because six were promoted to tier one
|
||||
* after it was written; read the counts as history, and `HEAP_BOUNDED` itself
|
||||
* as the answer. The re-entry condition the MEMORY section states — "if any of
|
||||
* the four ever diverges in what it ASKS, it earns an arm the same way" — is a
|
||||
* claim about growth, and this is the only thing in the file that can see it.
|
||||
*
|
||||
* NO FLOOR, and the reason is per language rather than uniform. rust reads 16 B
|
||||
* because it builds nothing, so any floor at all would be a floor on noise and
|
||||
* `1.5 x 0 B` is 0 — its bound is ABSOLUTE (1 MiB) for the same reason: a
|
||||
* multiplier on 16 B fails on the first byte of anything. The other eight are
|
||||
* NO FLOOR, and the reason is per language rather than uniform. rust (and zig)
|
||||
* reads 16 B because it builds nothing, so any floor at all would be a floor
|
||||
* on noise and `1.5 x 0 B` is 0 — its bound is ABSOLUTE (1 MiB) for the same
|
||||
* reason: a multiplier on 16 B fails on the first byte of anything. The other eight are
|
||||
* stable enough today to floor (0.24% peak-to-peak at worst over five runs).
|
||||
* The two this paragraph named as floor candidates, kotlin and dart, TOOK that
|
||||
* promotion: both now carry a ceiling and a recorded reading in tier one, which
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2600a1f6f8a042eb4f520a7870c34d9ca292765824537c3bc861b40dac8769a8
|
||||
7412edd9db56b4626c77fc09363e0d49a2f95763453756dceb82db6606280c28
|
||||
|
|
|
|||
|
|
@ -199,15 +199,16 @@
|
|||
}
|
||||
},
|
||||
"countArm": {
|
||||
"callDrops": 102,
|
||||
"totalDropsAllKinds": 148,
|
||||
"callDrops": 113,
|
||||
"totalDropsAllKinds": 159,
|
||||
"bySiteKind": {
|
||||
"call": 102,
|
||||
"call": 113,
|
||||
"read": 27,
|
||||
"write": 19
|
||||
},
|
||||
"callDropsByExtension": {
|
||||
".java": 49,
|
||||
".zig": 11,
|
||||
".cs": 8,
|
||||
".ts": 7,
|
||||
".cpp": 7,
|
||||
|
|
@ -224,14 +225,14 @@
|
|||
"callDropsByShape": {
|
||||
"chain-field": 60,
|
||||
"chain-call": 27,
|
||||
"no-chain": 12,
|
||||
"no-chain": 23,
|
||||
"chain-mixed": 2,
|
||||
"chain-unwrap": 1
|
||||
},
|
||||
"callDropsByOrigin": {
|
||||
"external": 44,
|
||||
"in-program": 36,
|
||||
"unknown": 22
|
||||
"in-program": 43,
|
||||
"unknown": 26
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
|
||||
},
|
||||
"cpp": {
|
||||
"fingerprint": "bf3587674267be1759e7c45abef143c3b81fe8629cfd17da5f8af40e83cc39ec",
|
||||
"fingerprint": "3aaee42523f02718795ba348782d78ec1418f35eff2e1b602422b192544fed96",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_2833_qualified_member_fields": "#2833 follow-up: the six per-qualifier-depth `field_declaration` type-binding rules for a QUALIFIED generic member are replaced by three depth-agnostic ones that match the outer `qualified_identifier` itself, with the qualifier reduced to its top-level tail in `interpret.ts` (`cppQualifiedTail`). This is a CAPTURE-LOGIC change and it moves the fingerprint in two places at once. (1) A qualified NON-generic member (`ns::Address addr;`, `std::string name;`) was captured by nothing at all and now binds \u2014 that is the whole +24 on the fixture corpus, every one of them a `std::string` member. (2) Qualifier depth is no longer enumerated, so `a::b::c::Repo<User>` (depth 3+) is captured where the old rules stopped at 2. Capture-name histogram, cpp-* corpus (278 files): `@type-binding.field` 8 -> 32, `@type-binding.name` and `@type-binding.type` 401 -> 425; synthetic DAO-20: `@type-binding.field` 40 -> 60, `@type-binding.name` and `@type-binding.type` 61 -> 81 (= 20 entities x the one `std::string name;` member the DAO unit already declared). NO OTHER TAG MOVED in either set \u2014 not one `@declaration.*`, `@scope.*` or `@reference.*` count \u2014 which is the property that says three rules replaced six without widening what a field_declaration matches. Measured over the 13 cpp-* fixture repos whose sources gained a binding, the distinct CALLS edge set is byte-identical before and after (32 edges): a reduced tail that names no workspace class binds nothing. Prior bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a -> db1156d81b3e3341faf5e938a4a34417f4fd246588b6150b4686481823262529; scaling 1.04 < 1.5.",
|
||||
"_rebaselined_2833_generic_member_fields": "#2833 review follow-up: the cpp DAO generator's unit gains two GENERIC member fields \u2014 `Repo<Entity_n> repo;` (bare template_type) and `std::vector<Entity_n> items;` (qualified_identifier wrapping a template_type) \u2014 plus the header declaring `template <typename T> class Repo`. CORPUS CHANGE, NOT A CAPTURE-LOGIC CHANGE: no extractor edit accompanies it. It exists because the corpus had ZERO template-typed member fields and, across 279 cpp-* fixtures, not one qualified generic member either, so BOTH rounds of new `field_declaration` type-binding rules landed with a byte-identical cpp fingerprint \u2014 the gate was structurally blind to the exact thing being changed. Measured under the new corpus, the three states now differ: pre-#2833 query 0e7cbda71360b7ff35dd76091c77f288d6af6a5cfa9185ad85a372aae8c85191 (4521 groups) -> the three template_type field rules de07d8b5300ed867b460918e16b4d80259c7eb6efc1034d32bebe9ff7cab126d (4541) -> the six qualified rules bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a (4561); under the OLD corpus all three were 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc. Capture-name histogram over the synthetic DAO-20: `@type-binding.field` 0 -> 40, `@declaration.field` 40 -> 80, `@type-binding.type`/`@type-binding.name` 20 -> 61, `@declaration.name` 104 -> 147 \u2014 40 = 20 entities x 2 fields, with the residual +1/+2/+3 attributable to the one-off header declaration; every `@reference.*` count is unchanged. Prior 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc -> bd47c82d09a83cbf0ac857f41876fa31d22304043735582e913bccde06cf2c1a; scaling 1.058 < 1.5. `c` is unaffected (3418cded..., unchanged).",
|
||||
|
|
@ -53,12 +53,13 @@
|
|||
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|\u2026` instead of `1|\u2026`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 7e27aea46f3e17f33c41babbe0ddd982d1ab5920f143864763e0a1c6aef882a5 -> 856d02f3f9d22cb973877211100aee8e052d4bc545922f78704b1a21ce49ddcc.",
|
||||
"capture_groups_small": 5021,
|
||||
"capture_groups_large": 16021,
|
||||
"capture_groups_fp": 4605,
|
||||
"fixture_count": 279
|
||||
"capture_groups_fp": 4601,
|
||||
"fixture_count": 279,
|
||||
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `choice.select(1)` / `choice.select(1.5)` (cpp-deleted-overload/main.cpp) no longer mint an INDIRECT `@callable-flow.invoke` (callee-kind binding) plus its synthetic `@reference.call.free` - that invoke was gated only by the same-named free `select` binding while the site is a genuine method call already captured as `@reference.call.member`, so the free-call duplicate is gone. capture_groups_fp 4605 -> 4601 (-4: 2 invokes + 2 synthetic call.free). Prior bf3587674267be1759e7c45abef143c3b81fe8629cfd17da5f8af40e83cc39ec -> 3aaee42523f02718795ba348782d78ec1418f35eff2e1b602422b192544fed96."
|
||||
},
|
||||
"csharp": {
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
|
||||
"fingerprint": "2930ef49fdce984a4c051409880bddfe8445e30e1c6bf802bd90a0a0f8f6b094",
|
||||
"fingerprint": "9c4d2ca55707a03185ea45e554462e81e6d83c9f8d0ea3cd63db5c362ad1bfec",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a -> 75cf380209fa7d1a8a3ec873be1a9424b4e5173be0b08234c2291e8521a9b3c1; scaling 1.061 < 1.5.",
|
||||
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: C# method-group/delegate callable flow facts with invocation-result suppression. Prior 2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9 -> f31544530924748f9aa37d11cec570bc10c3ddf9d9b237e6df7a17623fd2bb3a; scaling 1.115 < 1.5.",
|
||||
|
|
@ -69,7 +70,8 @@
|
|||
"capture_groups_small": 4259,
|
||||
"capture_groups_large": 13609,
|
||||
"capture_groups_fp": 2657,
|
||||
"fixture_count": 178
|
||||
"fixture_count": 178,
|
||||
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `string.Join(\", \", args)` (csharp-variadic-resolution/Utils/Logger.cs) loses `direct-callee-name|Join`; the argument fact itself is unchanged, capture_groups_fp 2657 (unchanged). Prior 2930ef49fdce984a4c051409880bddfe8445e30e1c6bf802bd90a0a0f8f6b094 -> 9c4d2ca55707a03185ea45e554462e81e6d83c9f8d0ea3cd63db5c362ad1bfec."
|
||||
},
|
||||
"rust": {
|
||||
"fingerprint": "e61653008ff2de506cfd47f905fa9eb22d82fbbfe94d2a1d8190c358211b57b7",
|
||||
|
|
@ -169,7 +171,7 @@
|
|||
"capture_groups_fp": 680
|
||||
},
|
||||
"typescript": {
|
||||
"fingerprint": "05d1dadd6c9ef35c74079fa50f341b1b36e4fb02c9a89dd1b59f32b7cfd5e633",
|
||||
"fingerprint": "fed04ed1d5db112387781e405da208ae6b3ab803889773b0455be96f01b893ff",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_2934_import_type_only": "#2934: `import-decomposer.ts` attaches a presence-only `@import.type-only` synthetic capture to specifiers `tsc` erases, so `check --cycles` can stop counting type-only edges as initialization cycles. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE \u2014 the tag is added to import matches that already existed, never a new match, the same shape as the #2747 receiver-chain rebaseline. Every count is unchanged: capture_groups_fp 2414, fixture_count 155, capture_groups_small/large 4503/14403 (those measure the SYNTHETIC scaling source, which has no imports at all). The fingerprint moves because `canonicalizeMatch` in measure.mjs hashes every TAG on every match, synthetics included, so one extra presence-only tag on an existing match rewrites that match's canonical string. Attribution is exact, not inferred: neutralizing ONLY the `m['@import.type-only'] = \u2026` assignment in import-decomposer.ts and re-running returns the fingerprint to c2fbf8a89e5686dd\u2026 byte-for-byte, so nothing else in the TypeScript capture stream moved. All 14 other languages report ok. Scaling 0.997 < 1.5. NOTE ON THE CONTROL: javascript did not move (2026993b\u2026, 43 fixtures), but it is a WEAK control here \u2014 `import type` is TypeScript-only syntax, so a JS corpus cannot express the construct and could not have drifted either way. It evidences no collateral damage, not the correctness of the TS change; the exact-attribution check above is what does that. Prior c2fbf8a89e5686dd1ff3659b20d41d8b05ebcc9790356e3653ee0c8ca5d365c8 -> f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f.",
|
||||
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 27f937bfb47d4bded316ea3c785ff659c8cd88a5761d928f113477a08c802c78 -> e05446620c5b80b7aae291cfdf32f693580fada2ae687124769b04a0c03bfe63; scaling 0.983 < 1.5.",
|
||||
|
|
@ -191,7 +193,8 @@
|
|||
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side \u2014 the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3.",
|
||||
"_rebaselined_type_parameter_shadowing_w2_8": "W2-8: `@declaration.type-parameters` is now captured on generic FUNCTIONS, generator functions and type ALIASES, not only on class/interface declarations. NO NEW CAPTURE NAME \u2014 verified by diffing the capture-name sets against the wave-1 branch, which returns empty; the tag already existed and simply fires on more declarations. That is the whole delta: capture_groups_fp 2338 -> 2371 (+33 occurrences of an existing tag) and fixture_count 151 -> 152 (one new fixture, typescript-type-parameters). capture_groups_small/large unchanged at 4503/14403, since those measure the synthetic scaling source this does not touch. Scaling 1.06 < 1.5. JavaScript is untouched \u2014 it has no type parameters \u2014 and its fingerprint does not move, which is the check that this is the TS declaration rules and not something broader. Prior f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7 -> 62c7f1bfbe568eed927fb78f00061ed5e49d12511fd8260648b876df386f3b4c.",
|
||||
"_rebaselined_2899_review_type_parameter_scope_fixtures": "PR #2899 review follow-up: FIXTURE-CORPUS GROWTH ONLY \u2014 no query rule changed and no capture name was added or removed. `typescript/query.ts` is byte-identical to the previous baseline; the type-parameter shadowing defect was fixed on the RESOLUTION side (`walkers.ts` gains a `declarationOpenedScope` gate so a declaration's `typeParameters` bind only inside the scope that declaration opened, and the `USES` guard moved from `graph-bridge/references-to-edges.ts` to `resolve-references.ts` where the spelled `site.name` is in hand). The fingerprint moves because measure.mjs fingerprints the whole `lang-resolution/typescript-*` fixture corpus and the regression tests add three files to `typescript-type-parameters/src/` (values.ts, aliased.ts, namespaced.ts) plus two scope-less generic aliases in shapes.ts. Per-file accounting sums exactly to the delta: shapes.ts 33->35 (+2), values.ts +11, aliased.ts +10, namespaced.ts +20 = +43. capture_groups_fp 2371 -> 2414; fixture_count 152 -> 155. capture_groups_small/large unchanged at 4503/14403 (they measure the SYNTHETIC scaling source, untouched). JAVASCRIPT IS THE CONTROL AND DID NOT MOVE (fingerprint 2026993b..., 43 fixtures) \u2014 which is the check that this is corpus growth and not a capture regression; all 14 other languages report `ok`. Scaling 0.976 < 1.5. Prior 62c7f1bfbe568eed927fb78f00061ed5e49d12511fd8260648b876df386f3b4c -> c2fbf8a89e5686dd1ff3659b20d41d8b05ebcc9790356e3653ee0c8ca5d365c8.",
|
||||
"_rebaselined_2953_workspace_fixture": "#2953 adds test/fixtures/lang-resolution/typescript-pnpm-workspace-imports, a pnpm monorepo of 12 .ts files, and the TypeScript capture corpus is collected from test/fixtures. CORPUS GROWTH ONLY, NOT A CAPTURE CHANGE: fixture_count 155 -> 167 and capture_groups_fp 2414 -> 2465 are the 12 new files' own matches; capture_groups_small/large are unchanged at 4503/14403 because those measure the SYNTHETIC scaling source, which the fixture corpus does not feed. Attribution is exact rather than inferred: moving that one fixture directory aside and re-running returns typescript to f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f byte-for-byte with fixture_count back at 155, and [scope-capture --check] PASSES for all 15 languages - so nothing in the TypeScript capture stream moved. #2953 changes import RESOLUTION, which runs after capture and feeds no capture tag. Prior f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f -> 05d1dadd6c9ef35c74079fa50f341b1b36e4fb02c9a89dd1b59f32b7cfd5e633."
|
||||
"_rebaselined_2953_workspace_fixture": "#2953 adds test/fixtures/lang-resolution/typescript-pnpm-workspace-imports, a pnpm monorepo of 12 .ts files, and the TypeScript capture corpus is collected from test/fixtures. CORPUS GROWTH ONLY, NOT A CAPTURE CHANGE: fixture_count 155 -> 167 and capture_groups_fp 2414 -> 2465 are the 12 new files' own matches; capture_groups_small/large are unchanged at 4503/14403 because those measure the SYNTHETIC scaling source, which the fixture corpus does not feed. Attribution is exact rather than inferred: moving that one fixture directory aside and re-running returns typescript to f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f byte-for-byte with fixture_count back at 155, and [scope-capture --check] PASSES for all 15 languages - so nothing in the TypeScript capture stream moved. #2953 changes import RESOLUTION, which runs after capture and feeds no capture tag. Prior f719163eb03a447c9e40ca316a905dd76cee82192a75a403df478ebbdc13e98f -> 05d1dadd6c9ef35c74079fa50f341b1b36e4fb02c9a89dd1b59f32b7cfd5e633.",
|
||||
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Drift: `await svc.verify<GuestPayload>(token, ...)` (typescript-generic-calls/src/guest.ts, member call) and `initializer()(() => {...})` (typescript-hof-callbacks/src/store.ts, call-of-call) lose `direct-callee-name`. `await verifyToken<AdminPayload>(token, ...)` (admin.ts/auth.ts) KEEPS `direct-callee-name|verifyToken`: tree-sitter-typescript parses `await f<T>(x)` as call_expression(function: await_expression(f), type_arguments, ...), and wrappedExpression now unwraps `await_expression` so the direct designator survives as it does for the un-awaited spelling. capture_groups_fp 2465 (unchanged). Prior 05d1dadd6c9ef35c74079fa50f341b1b36e4fb02c9a89dd1b59f32b7cfd5e633 -> fed04ed1d5db112387781e405da208ae6b3ab803889773b0455be96f01b893ff."
|
||||
},
|
||||
"javascript": {
|
||||
"fingerprint": "2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3",
|
||||
|
|
@ -209,7 +212,7 @@
|
|||
"_rebaselined_blind_spots_2856": "#2856 blind-spots series: the JS/TS SCOPE queries gained capture rules, so fingerprint drift is expected and additive. Verified before re-baselining by diffing the capture-name sets in both scope queries against origin/main: TypeScript gained exactly @reference.read.identifier (A2 bare-identifier reads in value positions) and @reference.type (R2-2 type references, so a declared contract stops reporting incoming:{}); JavaScript gained exactly @reference.read.identifier, @reference.read.destructured (R2-1c) and @reference.write.property-key (R2-1b record-construction writes). NOTHING was removed on either side \u2014 the delta is a pure superset, which is the check that no existing capture moved. capture_groups_small/large are unchanged (4503/14403) because those measure the SYNTHETIC scaling source, which this branch does not touch; only the fixture-corpus count moves. capture_groups_fp 2097 -> 2338 and fixture_count 146 -> 151 from 21 new lang-resolution fixtures. Scaling stayed linear and inside budget: typescript 1.116 < 1.5, javascript 1.010 < 1.5. Prior typescript ed92588e0fc7b28b3a0174339ac378b4dd85965fe007db1208dea97a65ce0571 -> f66a3e6f1e096431e7046505129a627deaa00ca0de5bc846b080591b397248f7; prior javascript 806f70ad3cce5fc849f6d06a08ace8a95f92a1ea84a2418fddabb1eef5846594 -> 2026993b81b873839dd2ef8797d9c14d9c48516b2b57b05ac17d8d43f2f4eba3."
|
||||
},
|
||||
"kotlin": {
|
||||
"fingerprint": "aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5",
|
||||
"fingerprint": "a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_interface_abstract_2885": "#2885: Kotlin interface property accessors stay in the capture set (groups still 5753/18403 and capture_groups_fp 2563) but Method isAbstract is now true for body-less interface properties, which changes accessor-plan identity in the fixture digest. Prior 82ae5e1f750580383344d4c84c400a290474528cd502be4af8cd56705819a683 -> aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5; CI scaling 0.838 < 1.5.",
|
||||
"_rebaselined_jvm_property_accessors_2885": "#2885: Kotlin val/var properties now emit JVM getter/setter scope and declaration captures, including data-class constructor properties and custom accessors. Synthetic scaling counts move 4753/15203 -> 5753/18403; fixture-corpus groups move 2367 -> 2563. Accessor declaration sidecars use the canonical @declaration.qualified_name key, preserve same-name owner identity, follow JvmAbi is-prefix naming, and suppress @JvmName-renamed accessors until their custom names are modeled. Prior f98e7e936afbce0e99588285cfc603bf945fd58c5de45271860509a5d90eb832 -> 82ae5e1f750580383344d4c84c400a290474528cd502be4af8cd56705819a683; scaling 0.869 < 1.5.",
|
||||
|
|
@ -228,6 +231,9 @@
|
|||
"capture_groups_small": 5753,
|
||||
"capture_groups_large": 18403,
|
||||
"capture_groups_fp": 2563,
|
||||
"fixture_count": 141
|
||||
"fixture_count": 141,
|
||||
"_rebaselined_1432_member_call_callee_name": "#1432 (Zig): the shared callable-flow reader no longer names a callee by simple name for a MEMBER call (`@callable-flow.direct-callee-name` requires a direct designator: `f(x)`, `ns.f(x)`), and a member call is a field-stored-callable invoke only when a MEMBER store (`o.f = handler`) or a declared callable-typed field is visible - a same-named plain binding no longer gates it. CAPTURE-EMISSION CHANGE, not fixture growth (fixture_count unchanged). Only drift: `users.map { it.name }.forEach { name -> println(name) }` (kotlin-lambda-scopes/App.kt) loses `direct-callee-name|forEach` (member call). capture_groups_fp 2334 (unchanged). Prior a184f8ff0ae40d246db855b63f7ff26bda3afac03e5f4c76e4593c7e2cefce54 -> 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284.",
|
||||
"_rebaselined_1432_rebase_onto_2960": "#1432 rebase onto main @ aac7515d: the kotlin fingerprint is a COMBINATION of two independent changes, so neither side of the merge conflict was correct on its own and resolving it by picking a side would have committed a fingerprint no run can reproduce. main's #2960 added four declared-package fixture files (fixture_count 137 -> 141, capture_groups_fp 2334 -> 2367); this branch's `_rebaselined_1432_member_call_callee_name` drops `direct-callee-name|forEach` from one member call. Recomputed under both: capture_groups_fp 2367 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only and moves no count), capture_groups_small/large stay 4753/15203 (the SYNTHETIC scaling source, which neither change touches), scaling 1.003 < 1.5, and the other 14 languages report ok against their committed baselines in the same run - which is the check that the rebase replayed nothing else into the capture stream. Attribution is exact rather than inferred: moving test/fixtures/lang-resolution/kotlin-import-package-evidence aside and re-running returns kotlin to 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284 byte-for-byte with fixture_count back at 137 and capture_groups_fp back at 2334 - this branch's pre-rebase value - so the whole delta is #2960's corpus growth layered on top, with nothing else moving. Prior (this branch, pre-rebase) 5a181af0dbc9451937da0964c40d3f3f9820914ca429d873bb5c812b5e2b9284 and (main) f98e7e936afbce0e99588285cfc603bf945fd58c5de45271860509a5d90eb832 -> 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5.",
|
||||
"_rebaselined_1432_merge_main_2885": "#1432 merge of main @ 212e007a: the kotlin fingerprint is again a COMBINATION of two independent changes \u2014 main's #2885 JVM property accessors / interface-abstract (4753/15203 -> 5753/18403, capture_groups_fp 2367 -> 2563) and this branch's `_rebaselined_1432_member_call_callee_name` (drops `direct-callee-name|forEach` from one member call). Neither side's value reproduces under the merged tree. Recomputed under both: counts 5753/18403/2563 and fixture_count 141 match main's committed counts EXACTLY (this branch's change is emission-only), scaling 1.061 < 1.5, and csharp / cpp / typescript measure byte-for-byte at this branch's committed values (main did not touch them since the merge-base) while the other 11 languages report ok \u2014 the check that the merge replayed nothing else into the capture stream. Prior (main) aeafc7a87402c933786ef582b7c98683b1822b78fa909e605cb97552867fa0d5 and (this branch) 973d702510002dda76166e017c5eca90cae37a139f5a512877b7a1b04ad19dc5 -> a9d3f0db7547ff47856159debf15d2a6f427efca97a6af27b4004174ed432132."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
gitnexus/package-lock.json
generated
21
gitnexus/package-lock.json
generated
|
|
@ -75,6 +75,7 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@huggingface/transformers": "^4.1.0",
|
||||
"@tree-sitter-grammars/tree-sitter-zig": "1.1.2",
|
||||
"onnxruntime-node": "^1.24.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -1760,6 +1761,26 @@
|
|||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tree-sitter-grammars/tree-sitter-zig": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tree-sitter-grammars/tree-sitter-zig/-/tree-sitter-zig-1.1.2.tgz",
|
||||
"integrity": "sha512-J0L31HZ2isy3F5zb2g5QWQOv2r/pbruQNL9ADhuQv2pn5BQOzxt80WcEJaYXBeuJ8GHxVT42slpCna8k1c8LOw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tree-sitter": "^0.22.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tree-sitter": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@huggingface/transformers": "^4.1.0",
|
||||
"@tree-sitter-grammars/tree-sitter-zig": "1.1.2",
|
||||
"onnxruntime-node": "^1.24.0"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
|
|
@ -127,6 +128,9 @@
|
|||
"sharp": ">=0.35.0",
|
||||
"@huggingface/transformers": {
|
||||
"onnxruntime-node": "$onnxruntime-node"
|
||||
},
|
||||
"@tree-sitter-grammars/tree-sitter-zig": {
|
||||
"tree-sitter": "$tree-sitter"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -257,6 +257,8 @@ export async function startWatchFileLoop(
|
|||
): Promise<WatchFileLoop> {
|
||||
let ignorePath = await createWatchIgnorePredicate(repoPath);
|
||||
let ignoreControlValid = true;
|
||||
let rearmPending = false;
|
||||
let closed = false;
|
||||
const queue = new WatchRefreshQueue(
|
||||
async (paths) => {
|
||||
if (paths.some(isIgnoreControlPath) || !ignoreControlValid) {
|
||||
|
|
@ -264,7 +266,7 @@ export async function startWatchFileLoop(
|
|||
try {
|
||||
ignorePath = await createWatchIgnorePredicate(repoPath);
|
||||
ignoreControlValid = true;
|
||||
watcher.add(repoPath);
|
||||
rearmPending = true;
|
||||
} catch (error) {
|
||||
ignoreControlValid = false;
|
||||
throw new WatchControlReloadError(
|
||||
|
|
@ -279,6 +281,11 @@ export async function startWatchFileLoop(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Re-arm before refreshing, never after: the refresh reads the whole
|
||||
// repository, so a write that lands while the replacement watcher is
|
||||
// arming is still picked up by this refresh, and a write that lands
|
||||
// afterwards is reported by the armed watcher.
|
||||
if (rearmPending) await rearmWatcher();
|
||||
await refresh(paths);
|
||||
},
|
||||
onError,
|
||||
|
|
@ -291,44 +298,87 @@ export async function startWatchFileLoop(
|
|||
},
|
||||
);
|
||||
|
||||
const watcher: FSWatcher = watch(repoPath, {
|
||||
ignoreInitial: true,
|
||||
atomic: true,
|
||||
followSymlinks: false,
|
||||
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
|
||||
ignored: (candidate, stats) => {
|
||||
const relative = repoRelativeWatchPath(repoPath, candidate);
|
||||
if (relative !== null && isAnalyzerOwnedWatchPath(relative)) return true;
|
||||
if (relative !== null && (isIgnoreControlPath(relative) || isConfigControlPath(relative))) {
|
||||
return false;
|
||||
const createWatcher = (): FSWatcher => {
|
||||
const created: FSWatcher = watch(repoPath, {
|
||||
ignoreInitial: true,
|
||||
atomic: true,
|
||||
followSymlinks: false,
|
||||
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
|
||||
ignored: (candidate, stats) => {
|
||||
const relative = repoRelativeWatchPath(repoPath, candidate);
|
||||
if (relative !== null && isAnalyzerOwnedWatchPath(relative)) return true;
|
||||
if (relative !== null && (isIgnoreControlPath(relative) || isConfigControlPath(relative))) {
|
||||
return false;
|
||||
}
|
||||
return ignorePath(candidate, stats?.isDirectory() ?? false);
|
||||
},
|
||||
});
|
||||
// Events from an instance being retired are kept: they overlap with the
|
||||
// replacement's coverage and the queue coalesces the duplicates.
|
||||
created.on('all', (event, changedPath) => {
|
||||
if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
|
||||
const relative = repoRelativeWatchPath(repoPath, changedPath);
|
||||
if (relative && isRelevantWatchPath(relative) && !isAnalyzerOwnedWatchPath(relative)) {
|
||||
queue.enqueue(relative);
|
||||
}
|
||||
return ignorePath(candidate, stats?.isDirectory() ?? false);
|
||||
},
|
||||
});
|
||||
watcher.on('all', (event, changedPath) => {
|
||||
if (event !== 'add' && event !== 'change' && event !== 'unlink') return;
|
||||
const relative = repoRelativeWatchPath(repoPath, changedPath);
|
||||
if (relative && isRelevantWatchPath(relative) && !isAnalyzerOwnedWatchPath(relative)) {
|
||||
queue.enqueue(relative);
|
||||
});
|
||||
created.on('error', (error) => {
|
||||
// Replacement failures before the swap are reported through `waitUntilReady`.
|
||||
if (created !== watcher) return;
|
||||
// Chokidar can surface a transient EPERM on Windows while an ignored
|
||||
// analyzer-owned path is replaced. Re-arm the watcher and force one
|
||||
// bounded catch-up refresh so a missed event cannot leave the graph
|
||||
// stale. Other watcher errors may mean coverage was lost and stay fatal.
|
||||
if (TRANSIENT_WATCH_ERROR_CODES.has((error as NodeJS.ErrnoException).code ?? '')) {
|
||||
rearmPending = true;
|
||||
queue.enqueue(WATCH_FULL_REFRESH_PATH);
|
||||
return;
|
||||
}
|
||||
onWatcherError(error);
|
||||
});
|
||||
return created;
|
||||
};
|
||||
|
||||
let watcher: FSWatcher = createWatcher();
|
||||
|
||||
// Chokidar emits `ready` once per instance and `add()` returns before the
|
||||
// rescan it starts has finished, with no signal for that completion. A file
|
||||
// an ignore-rule reload has just unignored is therefore still unregistered
|
||||
// when `add()` returns, and because `ignoreInitial` suppresses the `add` the
|
||||
// rescan would emit, an immediate rewrite of that file is dropped for good
|
||||
// (reproduced on chokidar 4 and 5). So re-arm by arming a replacement
|
||||
// watcher and awaiting its `ready` instead. The outgoing instance keeps
|
||||
// reporting until the replacement is armed, so the swap has no blind window,
|
||||
// and a replacement that fails to arm leaves the working instance in place.
|
||||
const rearmWatcher = async (): Promise<void> => {
|
||||
rearmPending = false;
|
||||
if (closed) return;
|
||||
const replacement = createWatcher();
|
||||
try {
|
||||
await waitUntilReady(replacement);
|
||||
} catch (error) {
|
||||
rearmPending = true;
|
||||
try {
|
||||
await replacement.close();
|
||||
} catch {
|
||||
// The instance never became live; the arm error is the one to report.
|
||||
}
|
||||
throw new WatchControlReloadError(
|
||||
new Error('Unable to re-arm the filesystem watcher', { cause: error }),
|
||||
);
|
||||
}
|
||||
});
|
||||
watcher.on('error', (error) => {
|
||||
// Chokidar can surface a transient EPERM on Windows while an ignored
|
||||
// analyzer-owned path is replaced. Re-arm the root and force one bounded
|
||||
// catch-up refresh so a missed event cannot leave the graph stale. Other
|
||||
// watcher errors may mean coverage was lost and remain fatal.
|
||||
if (TRANSIENT_WATCH_ERROR_CODES.has((error as NodeJS.ErrnoException).code ?? '')) {
|
||||
watcher.add(repoPath);
|
||||
queue.enqueue(WATCH_FULL_REFRESH_PATH);
|
||||
return;
|
||||
}
|
||||
onWatcherError(error);
|
||||
});
|
||||
const retired = watcher;
|
||||
watcher = replacement;
|
||||
await retired.close();
|
||||
// `close()` can land between arming the replacement and the swap above.
|
||||
if (closed) await replacement.close();
|
||||
};
|
||||
|
||||
try {
|
||||
await waitUntilReady(watcher);
|
||||
await queue.runInitial();
|
||||
} catch (error) {
|
||||
closed = true;
|
||||
await watcher.close();
|
||||
await queue.close();
|
||||
throw error;
|
||||
|
|
@ -337,6 +387,7 @@ export async function startWatchFileLoop(
|
|||
return {
|
||||
waitForIdle: () => queue.waitForIdle(),
|
||||
close: async () => {
|
||||
closed = true;
|
||||
await watcher.close();
|
||||
await queue.close();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,19 +7,23 @@
|
|||
* prebuilds activated via node-gyp-build. All can be skipped via
|
||||
* GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
|
||||
* soft-fail when no prebuild matches the host platform (and a source build was
|
||||
* unavailable / not attempted).
|
||||
* unavailable / not attempted). tree-sitter-zig is the one npm-installed
|
||||
* optionalDependency in the list; its `probe` overrides the vendored load.
|
||||
*
|
||||
* Either path produces the same observable: the .node binding is absent
|
||||
* at runtime. This helper detects that condition and surfaces a single
|
||||
* stderr line per missing grammar so users learn why .dart/.proto/.swift/.kt
|
||||
* stderr line per missing grammar so users learn why .dart/.proto/.swift/.kt/.zig
|
||||
* support is unavailable instead of silently getting a degraded index.
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { isGrammarRuntimeSkipped } from '../core/tree-sitter/parser-loader.js';
|
||||
import { requireVendoredGrammar } from '../core/tree-sitter/vendored-grammars.js';
|
||||
import { cliWarn } from './cli-message.js';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
||||
interface OptionalGrammar {
|
||||
/** Display name in warnings */
|
||||
name: string;
|
||||
|
|
@ -34,6 +38,12 @@ interface OptionalGrammar {
|
|||
* `.proto`, which is a gRPC-extractor concern, not a SupportedLanguages.
|
||||
*/
|
||||
language?: SupportedLanguages;
|
||||
/**
|
||||
* Availability probe. Defaults to `requireVendoredGrammar(pkg)`; grammars
|
||||
* that install from npm as an optionalDependency (zig) override it with a
|
||||
* plain `require` of the package.
|
||||
*/
|
||||
probe?: () => unknown;
|
||||
}
|
||||
|
||||
const OPTIONAL_GRAMMARS: OptionalGrammar[] = [
|
||||
|
|
@ -56,6 +66,14 @@ const OPTIONAL_GRAMMARS: OptionalGrammar[] = [
|
|||
extensions: ['.kt', '.kts'],
|
||||
language: SupportedLanguages.Kotlin,
|
||||
},
|
||||
{
|
||||
name: 'tree-sitter-zig',
|
||||
pkg: '@tree-sitter-grammars/tree-sitter-zig',
|
||||
extensions: ['.zig'],
|
||||
language: SupportedLanguages.Zig,
|
||||
// npm optionalDependency, not vendored — probe via plain require.
|
||||
probe: () => _require('@tree-sitter-grammars/tree-sitter-zig'),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -105,7 +123,11 @@ export function detectMissingOptionalGrammars(): MissingGrammar[] {
|
|||
continue;
|
||||
}
|
||||
try {
|
||||
requireVendoredGrammar(g.pkg);
|
||||
if (g.probe !== undefined) {
|
||||
g.probe();
|
||||
} else {
|
||||
requireVendoredGrammar(g.pkg);
|
||||
}
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { CallExtractionConfig } from '../../call-types.js';
|
||||
|
||||
export const zigCallConfig: CallExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
};
|
||||
42
gitnexus/src/core/ingestion/class-extractors/configs/zig.ts
Normal file
42
gitnexus/src/core/ingestion/class-extractors/configs/zig.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { ClassExtractionConfig, ClassLikeNodeLabel } from '../../class-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import {
|
||||
isZigFileStruct,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
zigContainerLabel,
|
||||
zigContainerName,
|
||||
} from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig containers (struct/enum/union/opaque) are anonymous in the grammar:
|
||||
*
|
||||
* const Point = struct { ... };
|
||||
* pub fn List(comptime T: type) type { return struct { ... }; }
|
||||
* fn build() void { const R = struct { ... }; sort(struct { fn lt … }.lt); }
|
||||
*
|
||||
* The identity is the binding name (first identifier of the parent
|
||||
* variable_declaration), the generic type constructor's name, or — for a
|
||||
* FUNCTION-LOCAL or ANONYMOUS container — a synthesized `host$Name` /
|
||||
* `host$N` (F8). `zigContainerName` is the single source shared with the
|
||||
* field/method extractors and the owner walk, so owner ids and node ids
|
||||
* agree by construction. Which of the (up to three) ZIG_QUERIES rules that
|
||||
* match one container gets to mint it is decided by the provider's
|
||||
* `shouldSkipDefinitionCapture` (`isZigRedundantContainerCapture`).
|
||||
*/
|
||||
const extractZigContainerName = (node: SyntaxNode, filePath?: string): string | undefined =>
|
||||
zigContainerName(node, filePath);
|
||||
|
||||
const extractZigContainerType = (node: SyntaxNode): ClassLikeNodeLabel | undefined => {
|
||||
// The file itself, when it declares top-level fields (file-struct); a
|
||||
// namespace-only file is not a type — `extract` then yields no symbol.
|
||||
if (node.type === 'source_file') return isZigFileStruct(node) ? 'Struct' : undefined;
|
||||
return zigContainerLabel(node);
|
||||
};
|
||||
|
||||
export const zigClassConfig: ClassExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
typeDeclarationNodes: [...ZIG_CONTAINER_TYPES, 'source_file'],
|
||||
extractName: extractZigContainerName,
|
||||
extractType: extractZigContainerType,
|
||||
};
|
||||
|
|
@ -57,6 +57,7 @@ const CLASS_LIKE_LABELS = new Set<ClassLikeNodeLabel>([
|
|||
'Interface',
|
||||
'Enum',
|
||||
'Record',
|
||||
'Union',
|
||||
]);
|
||||
|
||||
const extractScopeSegmentsFromNode = (
|
||||
|
|
@ -129,11 +130,15 @@ export function createClassExtractor(config: ClassExtractionConfig): ClassExtrac
|
|||
fallback?: {
|
||||
name?: string;
|
||||
type?: NodeLabel | null;
|
||||
filePath?: string;
|
||||
},
|
||||
): ExtractedClassSymbol | null => {
|
||||
if (!typeDeclarationSet.has(node.type)) return null;
|
||||
|
||||
const name = config.extractName?.(node) ?? extractTypeNameFromNode(node) ?? fallback?.name;
|
||||
const name =
|
||||
config.extractName?.(node, fallback?.filePath) ??
|
||||
extractTypeNameFromNode(node) ??
|
||||
fallback?.name;
|
||||
const type =
|
||||
config.extractType?.(node) ??
|
||||
DEFAULT_LABEL_BY_NODE_TYPE[node.type] ??
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { SyntaxNode } from './utils/ast-helpers.js';
|
|||
|
||||
export type ClassLikeNodeLabel = Extract<
|
||||
NodeLabel,
|
||||
'Class' | 'Struct' | 'Interface' | 'Enum' | 'Record'
|
||||
'Class' | 'Struct' | 'Interface' | 'Enum' | 'Record' | 'Union'
|
||||
>;
|
||||
|
||||
export interface ExtractedClassSymbol {
|
||||
|
|
@ -41,6 +41,8 @@ export interface ClassExtractor {
|
|||
fallback?: {
|
||||
name?: string;
|
||||
type?: NodeLabel | null;
|
||||
/** Repo-relative path of the file being extracted, when known. */
|
||||
filePath?: string;
|
||||
},
|
||||
): ExtractedClassSymbol | null;
|
||||
extractQualifiedName(node: SyntaxNode, simpleName: string): string | null;
|
||||
|
|
@ -72,7 +74,9 @@ export interface ClassExtractionConfig {
|
|||
*/
|
||||
qualifiedNodeId?: boolean;
|
||||
scopeNameNodeTypes?: string[];
|
||||
extractName?: (node: SyntaxNode) => string | undefined;
|
||||
/** `filePath` is supplied when known (definition-phase extraction) — a
|
||||
* language whose file IS a type names it from the path. */
|
||||
extractName?: (node: SyntaxNode, filePath?: string) => string | undefined;
|
||||
extractType?: (node: SyntaxNode) => ClassLikeNodeLabel | undefined;
|
||||
extractScopeSegments?: (node: SyntaxNode) => string[] | null | undefined;
|
||||
extractTemplateArguments?: (node: SyntaxNode) => string[] | undefined;
|
||||
|
|
|
|||
|
|
@ -242,3 +242,64 @@ export const rubyExportChecker: ExportChecker = (_node, _name) => true;
|
|||
|
||||
/** Dart: public if no leading underscore (convention, same as Python). */
|
||||
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');
|
||||
|
||||
/** Zig declaration node types whose `pub` / `export` keyword child marks the symbol public. */
|
||||
const ZIG_DECL_TYPES = new Set(['function_declaration', 'variable_declaration']);
|
||||
|
||||
/**
|
||||
* Zig: walk to the enclosing decl, scan its direct children for an unnamed `pub`
|
||||
* or `export` keyword token (tree-sitter-zig models both as anonymous keyword
|
||||
* children of function_declaration / variable_declaration). Two different
|
||||
* facts share this one flag, on purpose:
|
||||
* - `pub` is Zig-module visibility — reachable from another `.zig` file
|
||||
* through `@import`;
|
||||
* - `export` is C-ABI linkage (`export fn add(...)`) — the symbol lands in
|
||||
* the object file for FFI callers, and it never carries `pub`.
|
||||
* `isExported` means "visible outside this compilation unit" graph-wide (C uses
|
||||
* external linkage for the same flag), so both qualify. The `visibility`
|
||||
* property is the Zig-only fact and is `pub`-only — see `hasZigPubKeyword`:
|
||||
* an `export fn` without `pub` is public to C and PRIVATE to other Zig files.
|
||||
* Container fields (struct/enum variants) are public if their enclosing
|
||||
* variable_declaration is public.
|
||||
*
|
||||
* The walk stops at the FIRST declaration it reaches: a `fn` inside
|
||||
* `pub const T = struct { … }` carries its own `pub` (or not), independent of
|
||||
* the container's. Continuing up to the wrapper marked every private method of
|
||||
* a public container as exported.
|
||||
*/
|
||||
export const zigExportChecker: ExportChecker = (node, _name) => {
|
||||
let current: SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (ZIG_DECL_TYPES.has(current.type)) return hasZigVisibilityKeyword(current);
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Does this Zig declaration carry a `pub` or `export` keyword child? Feeds
|
||||
* `isExported` (visible outside the compilation unit — to Zig importers OR to
|
||||
* C callers).
|
||||
*/
|
||||
export function hasZigVisibilityKeyword(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.childCount; i++) {
|
||||
const child = declNode.child(i);
|
||||
if (child?.type === 'pub' || child?.type === 'export') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this Zig declaration carry a `pub` keyword child? Feeds `visibility`,
|
||||
* the Zig-module fact: per the language reference, only `pub` declarations are
|
||||
* accessible from another file via `@import`; `export` alone gives C linkage
|
||||
* and leaves the declaration private to Zig code. So `export fn c_add` reports
|
||||
* `isExported: true` (FFI surface) with `visibility: 'private'` (Zig surface)
|
||||
* — two facts, two properties, deliberately not one.
|
||||
*/
|
||||
export function hasZigPubKeyword(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.childCount; i++) {
|
||||
if (declNode.child(i)?.type === 'pub') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
58
gitnexus/src/core/ingestion/field-extractors/configs/zig.ts
Normal file
58
gitnexus/src/core/ingestion/field-extractors/configs/zig.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { ZIG_CONTAINER_TYPES, zigContainerName } from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig containers (struct/enum/union/opaque) are anonymous in tree-sitter-zig;
|
||||
* the binding name is the first identifier child of the parent
|
||||
* variable_declaration, or the enclosing generic type constructor's name —
|
||||
* `zigContainerName` is the single source.
|
||||
*/
|
||||
const extractZigOwnerName = (node: SyntaxNode, filePath?: string): string | undefined =>
|
||||
zigContainerName(node, filePath);
|
||||
|
||||
/**
|
||||
* Container fields appear as direct children of struct_declaration /
|
||||
* enum_declaration / union_declaration — there is no separate body wrapper
|
||||
* in this grammar, so `bodyNodeTypes` is empty and the generic factory's
|
||||
* "iterate immediate children" pass picks them up.
|
||||
*/
|
||||
export const zigFieldConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
// `source_file`: a file-struct's top-level fields belong to the file's Struct.
|
||||
typeDeclarationNodes: [...ZIG_CONTAINER_TYPES, 'source_file'],
|
||||
fieldNodeTypes: ['container_field'],
|
||||
bodyNodeTypes: [],
|
||||
defaultVisibility: 'public',
|
||||
extractOwnerName: extractZigOwnerName,
|
||||
|
||||
extractName(node) {
|
||||
const name = node.childForFieldName('name');
|
||||
// An empty container body (`struct {}`, `opaque {}`) is recovered by
|
||||
// tree-sitter-zig 1.1.2 as one container_field with a zero-width MISSING
|
||||
// identifier. Not a field — declining here keeps it out of the field map.
|
||||
if (name === null || name.text.length === 0) return undefined;
|
||||
return name.text;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
const typeNode = node.childForFieldName('type');
|
||||
return typeNode?.text?.trim();
|
||||
},
|
||||
|
||||
extractVisibility() {
|
||||
// Zig has no per-field visibility — fields inherit the container's
|
||||
// module-level visibility. Treat as public; the export checker decides
|
||||
// what the *container* exposes.
|
||||
return 'public';
|
||||
},
|
||||
|
||||
isStatic() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isReadonly() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
|
@ -34,7 +34,7 @@ export interface FieldExtractionConfig {
|
|||
/** Default visibility when no modifier is present */
|
||||
defaultVisibility: FieldVisibility;
|
||||
/** Extract owner type name from a type declaration node. */
|
||||
extractOwnerName?: (node: SyntaxNode) => string | undefined;
|
||||
extractOwnerName?: (node: SyntaxNode, filePath?: string) => string | undefined;
|
||||
/** Find body nodes inside a type declaration node. */
|
||||
findBodyNodes?: (node: SyntaxNode) => SyntaxNode[];
|
||||
/**
|
||||
|
|
@ -103,7 +103,8 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
|
|||
extract(node: SyntaxNode, context: FieldExtractorContext): ExtractedFields | null {
|
||||
if (!this.isTypeDeclaration(node)) return null;
|
||||
|
||||
const ownerFqn = config.extractOwnerName?.(node) ?? node.childForFieldName('name')?.text;
|
||||
const ownerFqn =
|
||||
config.extractOwnerName?.(node, context.filePath) ?? node.childForFieldName('name')?.text;
|
||||
if (!ownerFqn) return null;
|
||||
|
||||
const fields: FieldInfo[] = [];
|
||||
|
|
@ -148,6 +149,17 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
|
|||
if (result.length === 0 && bodyField) {
|
||||
result.push(bodyField);
|
||||
}
|
||||
// Grammars with no body wrapper at all: a config that declares NO
|
||||
// `bodyNodeTypes` (tree-sitter-zig's struct_declaration holds its
|
||||
// container_field children directly) uses the type-declaration node
|
||||
// itself as the body. The downstream walk filters by `fieldNodeTypes`,
|
||||
// so unrelated children are ignored. Deliberately NOT a fallback for
|
||||
// configs that do declare body wrappers: for them a node without its
|
||||
// wrapper is a bodiless declaration, and scanning it would change every
|
||||
// such language for no field it could find.
|
||||
if (result.length === 0 && bodyNodeSet.size === 0) {
|
||||
result.push(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
36
gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts
Normal file
36
gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Zig import resolution.
|
||||
*
|
||||
* Local-file imports (`@import("./foo.zig")`, `@import("foo.zig")`) resolve
|
||||
* relative to the importer. Bare names (`@import("bar")`) resolve through
|
||||
* build.zig.zon `.path` deps when a parsed ZigBuildZonConfig is available
|
||||
* (see language-config.ts `loadZigBuildConfig`). Everything unresolvable —
|
||||
* `std`, `builtin`, `root`, `.url`-based deps — returns an empty result so
|
||||
* it doesn't produce ghost import edges.
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
|
||||
import { resolveZigImportInternal } from '../zig.js';
|
||||
|
||||
const stripQuotes = (s: string): string => s.replace(/^['"]|['"]$/g, '');
|
||||
|
||||
export const zigImportStrategy: ImportResolverStrategy = (rawImportPath, filePath, ctx) => {
|
||||
// tree-sitter-zig captures the string with surrounding quotes.
|
||||
const stripped = stripQuotes(rawImportPath);
|
||||
const resolved = resolveZigImportInternal(
|
||||
filePath,
|
||||
stripped,
|
||||
ctx.allFilePaths,
|
||||
ctx.configs.zigBuildZon ?? null,
|
||||
);
|
||||
// Unresolvable (stdlib / builtin / .url dep / missing file): stop the
|
||||
// chain with an empty result rather than falling through to suffix
|
||||
// matching, which could ghost-match an unrelated same-named file.
|
||||
return { kind: 'files', files: resolved ? [resolved] : [] };
|
||||
};
|
||||
|
||||
export const zigImportConfig: ImportResolutionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
strategies: [zigImportStrategy],
|
||||
};
|
||||
210
gitnexus/src/core/ingestion/import-resolvers/zig.ts
Normal file
210
gitnexus/src/core/ingestion/import-resolvers/zig.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Zig module import resolution — internal helpers.
|
||||
*
|
||||
* Zig imports take three shapes:
|
||||
* const std = @import("std"); → stdlib, unresolvable
|
||||
* const builtin = @import("builtin"); → compiler builtin, unresolvable
|
||||
* const root = @import("root"); → user's main module, unresolvable here
|
||||
* const foo = @import("./foo.zig"); → relative path
|
||||
* const foo = @import("foo.zig"); → also relative (Zig treats unprefixed
|
||||
* paths with a `.zig` extension as
|
||||
* filesystem-relative to the importer)
|
||||
* const lp = @import("lightpanda"); → the repo's OWN module, declared by
|
||||
* its root build.zig (`b.addModule`)
|
||||
* const bar = @import("bar"); → package dep declared in build.zig.zon
|
||||
*
|
||||
* Bare-name resolution is handled when a parsed ZigBuildZonConfig is
|
||||
* supplied (see `loadZigBuildConfig`). The import table of the build module
|
||||
* the importer belongs to comes first (`buildModules` — per-module
|
||||
* `addImport` aliases, see `zigModulesContaining`), then the root
|
||||
* build.zig's named modules flattened repo-wide (`rootModules`, name → root
|
||||
* file — a repo with no build.zig.zon still resolves them). `.url`-based deps unpack into a build cache outside
|
||||
* the repo and so are returned as null; `.path`-based deps are resolved
|
||||
* through the root the dep's own build.zig declares, then the conventional
|
||||
* `<dep_root>/src/root.zig`, `<dep_root>/src/<name>.zig`,
|
||||
* `<dep_root>/src/main.zig` layouts.
|
||||
*/
|
||||
|
||||
import {
|
||||
normalizeZigDepPath,
|
||||
type ZigBuildModule,
|
||||
type ZigBuildZonConfig,
|
||||
} from '../language-config.js';
|
||||
|
||||
const ZIG_STDLIB_NAMES = new Set(['std', 'builtin', 'root']);
|
||||
|
||||
/**
|
||||
* The build module(s) a source file belongs to: the module whose ROOT the
|
||||
* file is, else the module(s) whose root's directory is the deepest prefix
|
||||
* of the file's path. Membership is not declared anywhere static — a module
|
||||
* is its root plus whatever that root reaches through relative imports —
|
||||
* so the directory is the proxy, and several modules may share one
|
||||
* (`src/main.zig` executable beside `src/root.zig` library is the `zig init`
|
||||
* layout). A root file is unambiguous by construction: it is its own module.
|
||||
*/
|
||||
function zigModulesContaining(
|
||||
currentFile: string,
|
||||
modules: readonly ZigBuildModule[],
|
||||
): ZigBuildModule[] {
|
||||
const own = modules.filter((m) => m.root === currentFile);
|
||||
if (own.length > 0) return own;
|
||||
let best = -1;
|
||||
let out: ZigBuildModule[] = [];
|
||||
for (const mod of modules) {
|
||||
const slash = mod.root.lastIndexOf('/');
|
||||
const dir = slash === -1 ? '' : mod.root.slice(0, slash);
|
||||
if (dir !== '' && !currentFile.startsWith(`${dir}/`)) continue;
|
||||
if (dir.length > best) {
|
||||
best = dir.length;
|
||||
out = [mod];
|
||||
} else if (dir.length === best) {
|
||||
out.push(mod);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare `@import("<name>")` through the containing module(s)' own import
|
||||
* tables. `undefined` when no containing module binds the name (the caller
|
||||
* falls back to the repo-wide tables); `null` when the containing modules
|
||||
* DISAGREE — two same-directory modules that bind one alias to different
|
||||
* roots — or when ANY containing module binds the alias to a root that is
|
||||
* not indexed (a generated or skipped file), including the mixed case
|
||||
* where a sibling module's target *is* indexed. Both are fail-closed on purpose: the module's
|
||||
* own table is the authority for the alias, and falling through to the
|
||||
* repo-wide map would reintroduce the first-wins answer this table exists
|
||||
* to remove, under a name the module never meant.
|
||||
*/
|
||||
function resolveThroughBuildModules(
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
allFiles: ReadonlySet<string>,
|
||||
modules: readonly ZigBuildModule[],
|
||||
): string | null | undefined {
|
||||
const targets = new Set<string>();
|
||||
for (const mod of zigModulesContaining(currentFile, modules)) {
|
||||
const target = mod.imports.get(importPath);
|
||||
if (target === undefined) continue;
|
||||
if (!allFiles.has(target)) return null;
|
||||
targets.add(target);
|
||||
}
|
||||
if (targets.size === 0) return undefined;
|
||||
if (targets.size > 1) return null;
|
||||
return targets.values().next().value ?? null;
|
||||
}
|
||||
|
||||
/** Resolve a Zig @import argument to a file path in the repository.
|
||||
* Returns null when the import is a stdlib / builtin / root reference,
|
||||
* an unresolvable build.zig.zon package dep, or genuinely unresolvable.
|
||||
*
|
||||
* `buildZon` (optional) supplies the parsed `.dependencies` map from
|
||||
* build.zig.zon. */
|
||||
export function resolveZigImportInternal(
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
allFiles: ReadonlySet<string>,
|
||||
buildZon?: ZigBuildZonConfig | null,
|
||||
): string | null {
|
||||
// Stdlib / compiler builtin / root — not resolvable from source files alone.
|
||||
if (ZIG_STDLIB_NAMES.has(importPath)) return null;
|
||||
|
||||
// Normalize path separators for the path arithmetic below. The `.zig`
|
||||
// extension is kept as written: the first candidate is the path as spelled
|
||||
// and only the fallback appends `.zig` for extension-less spellings.
|
||||
const trimmed = importPath.replace(/\\/g, '/');
|
||||
|
||||
// Absolute paths point outside the repository (Zig itself rejects
|
||||
// `@import("/abs.zig")` as an import outside the module path). Splitting
|
||||
// would drop the empty leading component and read `/foo.zig` as an
|
||||
// importer-relative `foo.zig`, fabricating an in-repo edge.
|
||||
// A drive-qualified spelling (`C:/foo.zig`, normalized from `C:\foo.zig`)
|
||||
// is absolute too: it carries a `/`, so without this guard it would take
|
||||
// the importer-relative branch and probe `src/C:/foo.zig`. Same test as
|
||||
// `normalizeZigDepPath`.
|
||||
if (trimmed.startsWith('/') || /^[A-Za-z]:\//.test(trimmed)) return null;
|
||||
|
||||
// Path-bearing import: resolve relative to the current file's directory.
|
||||
// Zig allows both "./foo.zig" and "foo.zig" — both are filesystem-relative.
|
||||
if (trimmed.endsWith('.zig') || trimmed.includes('/')) {
|
||||
const currentDir = currentFile.split('/').slice(0, -1);
|
||||
const parts = trimmed.split('/');
|
||||
for (const part of parts) {
|
||||
if (part === '' || part === '.') continue;
|
||||
if (part === '..') {
|
||||
// Above the repository root: the import names a file outside the
|
||||
// repo, so it must not alias a same-named root file (`../bar.zig`
|
||||
// from `main.zig` is NOT `bar.zig`).
|
||||
if (currentDir.length === 0) return null;
|
||||
currentDir.pop();
|
||||
} else {
|
||||
currentDir.push(part);
|
||||
}
|
||||
}
|
||||
const candidate = currentDir.join('/');
|
||||
if (allFiles.has(candidate)) return candidate;
|
||||
if (allFiles.has(candidate + '.zig')) return candidate + '.zig';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bare name without extension or slashes (e.g. @import("bar")).
|
||||
if (buildZon) {
|
||||
// First the import table of the build module the importer belongs to:
|
||||
// an alias is scoped to the module whose `addImport` declared it, so
|
||||
// this is the only table that can tell `app`'s `@import("config")` from
|
||||
// `tool`'s. A disagreement between same-directory modules is `null`
|
||||
// here and stops the chain — the repo-wide fallbacks below would only
|
||||
// reintroduce the first-wins answer.
|
||||
if (buildZon.buildModules !== undefined && buildZon.buildModules.length > 0) {
|
||||
const scoped = resolveThroughBuildModules(
|
||||
currentFile,
|
||||
importPath,
|
||||
allFiles,
|
||||
buildZon.buildModules,
|
||||
);
|
||||
if (scoped !== undefined) return scoped;
|
||||
}
|
||||
|
||||
// The repo's own modules, as its root build.zig names them
|
||||
// (`b.addModule("lightpanda", .{ .root_source_file = b.path("src/lightpanda.zig") })`),
|
||||
// take precedence: that declaration is exactly what an in-repo
|
||||
// `@import("lightpanda")` means, whatever the zon says. `std` / `builtin`
|
||||
// / `root` were rejected above and can never be reached from here.
|
||||
// Authoritative when it binds the name: a root that is not indexed (a
|
||||
// generated or skipped file) is `null`, never a fall-through to a
|
||||
// same-named zon dep — that would be a different declaration answering
|
||||
// under the name (gitnexus-check on fe24b37f; same rule as the
|
||||
// build-module tables above).
|
||||
const rootModule = buildZon.rootModules?.get(importPath);
|
||||
if (rootModule !== undefined) return allFiles.has(rootModule) ? rootModule : null;
|
||||
|
||||
// Then build.zig.zon `.path` deps.
|
||||
const depPath = buildZon.pathDeps.get(importPath);
|
||||
if (depPath) {
|
||||
const normalized = normalizeZigDepPath(depPath);
|
||||
if (normalized !== null) {
|
||||
// What the dep's own build.zig declares comes first (its
|
||||
// `addModule` root_source_file — see `parseZigBuildModuleRoots`), then
|
||||
// the conventional layouts: `src/root.zig` (the `zig init` library
|
||||
// root since 0.12), `src/<name>.zig` (older name-matched convention),
|
||||
// `src/main.zig` (executables / older inits). A dep at `.path = "."`
|
||||
// (the repo itself) normalizes to '' and must not grow a leading slash
|
||||
// — `allFiles` keys are repo-relative.
|
||||
const prefix = normalized === '' ? '' : `${normalized}/`;
|
||||
const candidates = [
|
||||
...(buildZon.moduleRoots?.get(importPath) ?? []),
|
||||
`${prefix}src/root.zig`,
|
||||
`${prefix}src/${importPath}.zig`,
|
||||
`${prefix}src/main.zig`,
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (allFiles.has(c)) return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bare name with no resolution (no build.zig / build.zig.zon, .url-based dep, generated module, or
|
||||
// unconventional layout).
|
||||
return null;
|
||||
}
|
||||
|
|
@ -179,6 +179,69 @@ export interface SwiftPackageConfig {
|
|||
targets: Map<string, string>;
|
||||
}
|
||||
|
||||
/** Zig package config parsed from build.zig.zon and the root build.zig */
|
||||
export interface ZigBuildZonConfig {
|
||||
/**
|
||||
* Map of dependency name -> the raw `.path = "..."` value, exactly as
|
||||
* written in build.zig.zon (relative to the repo root, and possibly
|
||||
* escaping it: `../local_dep`). Consumers normalize — see
|
||||
* `normalizeZigDepPath` below, which rejects absolute
|
||||
* and repo-escaping values. `.url`-based deps cannot be resolved to a
|
||||
* repo-local file (they unpack into a build cache outside the repo) and so
|
||||
* are not included here.
|
||||
*/
|
||||
pathDeps: Map<string, string>;
|
||||
/**
|
||||
* Per path-dep: repo-relative root source files the dep's own `build.zig`
|
||||
* declares (`b.addModule("name", .{ .root_source_file = b.path("src/x.zig")
|
||||
* })`), keyed by dep name, in file order. Entries whose module name matches
|
||||
* the dep name come first — that is the module a consumer's
|
||||
* `@import("<dep>")` maps to under the ecosystem convention that the zon key
|
||||
* and the module name agree. Absent (or empty) when the dep has no readable
|
||||
* `build.zig`; the resolver then falls back to the conventional layouts.
|
||||
*/
|
||||
moduleRoots?: Map<string, readonly string[]>;
|
||||
/**
|
||||
* Modules the repo's OWN root `build.zig` declares under an importable
|
||||
* name, module name → repo-relative root source file
|
||||
* (`b.addModule("lp", .{ .root_source_file = b.path("src/lp.zig") })`, or a
|
||||
* `createModule` binding later named through `addImport("lp", binding)`).
|
||||
* These are what an in-repo `@import("lp")` means — the most common shape in
|
||||
* single-package repos, where every file imports the package's own root
|
||||
* module by name. Independent of `build.zig.zon`: a repo with a `build.zig`
|
||||
* and no zon still resolves them. See `parseZigRootModules`.
|
||||
*/
|
||||
rootModules?: Map<string, string>;
|
||||
/**
|
||||
* Every build module the root `build.zig` declares, each with ITS OWN
|
||||
* import table — `addModule` / `createModule` roots and the root modules of
|
||||
* `addExecutable` / `addLibrary` / `addTest` artifacts, with the aliases
|
||||
* their `addImport("<alias>", …)` calls and `.imports = &.{ … }` fields
|
||||
* bind. `rootModules` flattens all of those into one first-wins map, which
|
||||
* is wrong as soon as two modules bind one alias to different roots (an
|
||||
* `app` and a `tool` executable that each `addImport("config", …)` their
|
||||
* own `config.zig`): the second module's files resolved to the first
|
||||
* module's target. The resolver walks a source file to its containing
|
||||
* module(s) and consults their tables first — see
|
||||
* `resolveZigImportInternal` / `parseZigBuildModules`.
|
||||
*/
|
||||
buildModules?: readonly ZigBuildModule[];
|
||||
}
|
||||
|
||||
/** One build module of the root `build.zig` — see `ZigBuildZonConfig.buildModules`. */
|
||||
export interface ZigBuildModule {
|
||||
/** The `addModule("<name>", …)` name; absent for `createModule` bindings
|
||||
* and artifact root modules, which are reachable only through aliases. */
|
||||
readonly name?: string;
|
||||
/** Repo-relative root source file (`b.path("src/x.zig")`). */
|
||||
readonly root: string;
|
||||
/** Alias → repo-relative root source file, as this module's own
|
||||
* `addImport` calls and `.imports` field declare it. Includes aliases to
|
||||
* a path dep's module (`addImport("api", dep.module("core"))`) when the
|
||||
* dep's build.zig declares that module. */
|
||||
readonly imports: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LANGUAGE-SPECIFIC CONFIG LOADERS
|
||||
// ============================================================================
|
||||
|
|
@ -548,6 +611,703 @@ export async function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftPac
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Zig build configuration a repo's `build.zig.zon` + root `build.zig`
|
||||
* declare: `.path` deps (and the roots their own build.zig names) from the
|
||||
* zon, and the repo's own named modules from the root build.zig. Either file
|
||||
* may be missing — a repo with a `build.zig` but no `build.zig.zon` still
|
||||
* resolves `@import("<own module>")`. Null only when neither contributes.
|
||||
*
|
||||
* `build.zig.zon` is Zig source (an anonymous-struct literal), not JSON.
|
||||
* Rather than pull in a tree-sitter parse for one file, we use a small
|
||||
* regex-based extractor that handles the common shapes:
|
||||
*
|
||||
* .dependencies = .{
|
||||
* .ziggit_pkg = .{
|
||||
* .url = "https://...",
|
||||
* .hash = "1220...",
|
||||
* },
|
||||
* .local_dep = .{
|
||||
* .path = "../local_dep",
|
||||
* },
|
||||
* },
|
||||
*
|
||||
* Limitations (intentional — bail to null on anything weirder):
|
||||
* - Only the top-level `.dependencies = .{ ... }` block is parsed (brace
|
||||
* depth 1); a same-named field nested in another struct is ignored.
|
||||
* - Each dep entry is matched by a single shape: `.<name> = .{ ... }`
|
||||
* where `<name>` is a bare identifier (no `@"…"` quoted form).
|
||||
* - Only `.path = "..."` is captured. `.url` deps are left unresolved
|
||||
* because their unpacked location lives outside the repo
|
||||
* (.zig-cache/p/<hash>/ or ~/.cache/zig/p/<hash>/) and is therefore
|
||||
* not in our `allFilePaths` set.
|
||||
* - `//` line comments are stripped before scanning (string-aware, so a
|
||||
* `//` inside `.url = "https://…"` survives), and brace matching skips
|
||||
* string literals — a commented-out `.path` or a `}` inside a comment
|
||||
* or string cannot declare a dep or truncate the block.
|
||||
*/
|
||||
export async function loadZigBuildConfig(repoRoot: string): Promise<ZigBuildZonConfig | null> {
|
||||
let config: ZigBuildZonConfig | null = null;
|
||||
try {
|
||||
const raw = await fs.readFile(path.join(repoRoot, 'build.zig.zon'), 'utf-8');
|
||||
config = parseZigBuildZon(raw);
|
||||
} catch {
|
||||
// No zon (or unreadable): the root build.zig may still declare modules.
|
||||
}
|
||||
|
||||
// The repo's own importable modules, from its root build.zig. Independent
|
||||
// of the zon: `@import("<own module>")` is how single-package repos refer
|
||||
// to their root file from every other file.
|
||||
let rootModules: Map<string, string> | undefined;
|
||||
let rootBuildZig: string | null = null;
|
||||
try {
|
||||
rootBuildZig = await fs.readFile(path.join(repoRoot, 'build.zig'), 'utf-8');
|
||||
const parsed = parseZigRootModules(rootBuildZig);
|
||||
if (parsed.size > 0) rootModules = parsed;
|
||||
} catch {
|
||||
// No root build.zig — nothing to declare.
|
||||
}
|
||||
|
||||
if (config === null) {
|
||||
if (rootBuildZig === null) return null;
|
||||
// No zon: no path deps, so `dep.module(…)` operands resolve to nothing.
|
||||
const buildModules = parseZigBuildModules(rootBuildZig);
|
||||
if (!rootModules && buildModules.length === 0) return null;
|
||||
return {
|
||||
pathDeps: new Map(),
|
||||
...(rootModules ? { rootModules } : {}),
|
||||
...(buildModules.length > 0 ? { buildModules } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// A path dep's importable root is whatever ITS build.zig declares, not a
|
||||
// fixed layout: read `root_source_file` per `addModule` and remember it
|
||||
// repo-relative. Best effort — an unreadable build.zig just leaves the
|
||||
// conventional-layout fallback in place.
|
||||
const moduleRoots = new Map<string, readonly string[]>();
|
||||
// Per path dep: the modules its build.zig NAMES (`addModule("core", …)`),
|
||||
// repo-relative — what a root-build.zig `dep.module("core")` operand means.
|
||||
const depModules = new Map<string, ReadonlyMap<string, string>>();
|
||||
for (const [depName, depPath] of config.pathDeps) {
|
||||
const rel = normalizeZigDepPath(depPath);
|
||||
if (rel === null) continue;
|
||||
let buildZig: string;
|
||||
try {
|
||||
buildZig = await fs.readFile(path.join(repoRoot, rel, 'build.zig'), 'utf-8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const prefixed = (r: string): string => (rel === '' ? r : `${rel}/${r}`);
|
||||
const roots = parseZigBuildModuleRoots(buildZig, depName).map(prefixed);
|
||||
if (roots.length > 0) moduleRoots.set(depName, roots);
|
||||
const named = new Map<string, string>();
|
||||
for (const mod of parseZigBuildModules(buildZig)) {
|
||||
if (mod.name !== undefined && !named.has(mod.name)) named.set(mod.name, prefixed(mod.root));
|
||||
}
|
||||
if (named.size > 0) depModules.set(depName, named);
|
||||
}
|
||||
const buildModules = rootBuildZig === null ? [] : parseZigBuildModules(rootBuildZig, depModules);
|
||||
return {
|
||||
...config,
|
||||
...(moduleRoots.size > 0 ? { moduleRoots } : {}),
|
||||
...(rootModules ? { rootModules } : {}),
|
||||
...(buildModules.length > 0 ? { buildModules } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a `.path` value from build.zig.zon into a repo-relative form.
|
||||
* Returns null for paths that escape the repo root (start with `..`) or
|
||||
* are absolute — those point to files we don't index. `.` / `./` normalize
|
||||
* to the empty string (the repo root itself). Shared with the import
|
||||
* resolver so both sides agree on which deps are in-repo.
|
||||
*/
|
||||
export function normalizeZigDepPath(depPath: string): string | null {
|
||||
// Normalize separators BEFORE the absolute check so every Windows spelling
|
||||
// is visible to it: POSIX (`/x`), drive (`C:\x`, `C:/x`), root-relative
|
||||
// (`\x` → `/x`) and UNC (`\\server\share` → `//server/share`) paths all
|
||||
// point outside the repository.
|
||||
const normalized = depPath.replace(/\\/g, '/');
|
||||
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)) return null;
|
||||
const parts: string[] = [];
|
||||
for (const part of normalized.split('/')) {
|
||||
if (part === '' || part === '.') continue;
|
||||
if (part === '..') {
|
||||
if (parts.length === 0) return null;
|
||||
parts.pop();
|
||||
} else {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* The `root_source_file` paths a `build.zig` declares, dep-relative, with the
|
||||
* module whose `addModule("<name>", …)` name equals `preferredName` first.
|
||||
*
|
||||
* Reads two shapes, which between them cover `zig init` output and the
|
||||
* common hand-written build scripts:
|
||||
* - `b.addModule("name", .{ .root_source_file = b.path("src/root.zig") })`
|
||||
* - any other `.root_source_file = b.path("…")` (exe/lib/test artifacts),
|
||||
* kept as unnamed fallbacks in file order.
|
||||
* A `.zig` under `b.path` is required — `.{ .cwd_relative = … }` and
|
||||
* `LazyPath` values computed at build time are not resolvable statically and
|
||||
* are skipped. Duplicates collapse to the first occurrence.
|
||||
*/
|
||||
export function parseZigBuildModuleRoots(buildZig: string, preferredName: string): string[] {
|
||||
const named: string[] = [];
|
||||
const unnamed: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (into: string[], p: string): void => {
|
||||
const norm = normalizeZigDepPath(p);
|
||||
if (norm === null || norm === '' || !norm.endsWith('.zig') || seen.has(norm)) return;
|
||||
seen.add(norm);
|
||||
into.push(norm);
|
||||
};
|
||||
const rootRe = /\.root_source_file\s*=\s*b\.path\(\s*"([^"\n]+)"\s*\)/;
|
||||
// The named module: scan the whole `addModule(…)` argument list, balanced
|
||||
// on parentheses, so a nested field before `.root_source_file` (`.imports =
|
||||
// &.{ .{ … } }`) does not end the match early — a `[^}]*` regex stopped at
|
||||
// that inner `}` and silently demoted the module to an unnamed fallback.
|
||||
const text = stripZonComments(buildZig);
|
||||
const mask = zonStringMask(text);
|
||||
const callRe = /\baddModule\s*\(/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = callRe.exec(text)) !== null) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
const argsStart = m.index + m[0].length;
|
||||
const argsEnd = findZigParenEnd(text, argsStart);
|
||||
if (argsEnd < 0) break;
|
||||
const args = text.slice(argsStart, argsEnd);
|
||||
const nameMatch = /^\s*"([^"\n]+)"\s*,/.exec(args);
|
||||
if (nameMatch?.[1] !== preferredName) continue;
|
||||
const root = zigTopLevelStaticRoot(args);
|
||||
if (root !== null) add(named, root);
|
||||
}
|
||||
const anyRe = new RegExp(rootRe.source, 'g');
|
||||
while ((m = anyRe.exec(text)) !== null) add(unnamed, m[1]!);
|
||||
return [...named, ...unnamed];
|
||||
}
|
||||
|
||||
/**
|
||||
* The importable modules a repo's ROOT `build.zig` declares, module name →
|
||||
* repo-relative root source file. Static scan (no execution) of two shapes:
|
||||
*
|
||||
* - `b.addModule("<name>", .{ .root_source_file = b.path("<p>.zig"), … })`
|
||||
* names the module directly;
|
||||
* - `const m = b.createModule(.{ .root_source_file = b.path("<p>.zig"), … })`
|
||||
* (or `const m = b.addModule(…)`) bound to an identifier and later named
|
||||
* by `x.addImport("<name>", m)` or `.imports = &.{ .{ .name = "<name>",
|
||||
* .module = m } }`.
|
||||
*
|
||||
* Deliberately NOT resolved — they are not in-repo source files: modules whose
|
||||
* root is not a static `b.path("….zig")` (generated `opts.createModule()` from
|
||||
* `addOptions`, `translate_c.createModule()`, `.cwd_relative` / computed
|
||||
* LazyPaths), `addImport("<name>", dep.module("…"))` (a `.url` / path dep,
|
||||
* handled through the zon), and aliases whose module operand is anything but a
|
||||
* bare identifier bound above (`config.lp_module`). Comments are stripped and
|
||||
* string literals skipped; the first declaration of a name wins.
|
||||
*/
|
||||
export function parseZigRootModules(buildZig: string): Map<string, string> {
|
||||
const text = stripZonComments(buildZig);
|
||||
const mask = zonStringMask(text);
|
||||
const modules = new Map<string, string>();
|
||||
// identifier → repo-relative root, for `const m = b.createModule(…)` /
|
||||
// `const m = b.addModule(…)` bindings later named via addImport.
|
||||
const bindings = new Map<string, string>();
|
||||
const callRe = /\b(addModule|createModule)\s*\(/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = callRe.exec(text)) !== null) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
const argsStart = m.index + m[0].length;
|
||||
const argsEnd = findZigParenEnd(text, argsStart);
|
||||
if (argsEnd < 0) break;
|
||||
const args = text.slice(argsStart, argsEnd);
|
||||
const root = zigTopLevelStaticRoot(args);
|
||||
if (root === null) continue;
|
||||
if (m[1] === 'addModule') {
|
||||
const nameMatch = /^\s*"([^"\n]+)"\s*,/.exec(args);
|
||||
if (nameMatch && !modules.has(nameMatch[1]!)) modules.set(nameMatch[1]!, root);
|
||||
}
|
||||
const binding = ZIG_MODULE_BINDING_RE.exec(text.slice(0, m.index));
|
||||
if (binding && !bindings.has(binding[1]!)) bindings.set(binding[1]!, root);
|
||||
}
|
||||
if (bindings.size === 0) return modules;
|
||||
const aliasRes = [
|
||||
/\.addImport\(\s*"([^"\n]+)"\s*,\s*([A-Za-z_]\w*)\s*\)/g,
|
||||
/\.name\s*=\s*"([^"\n]+)"\s*,\s*\.module\s*=\s*([A-Za-z_]\w*)\s*[,}]/g,
|
||||
];
|
||||
for (const re of aliasRes) {
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
const root = bindings.get(m[2]!);
|
||||
if (root !== undefined && !modules.has(m[1]!)) modules.set(m[1]!, root);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every build module the ROOT `build.zig` declares, each with its OWN import
|
||||
* table (`ZigBuildModule`). Static scan (no execution) of:
|
||||
*
|
||||
* - `b.addModule("<name>", .{ .root_source_file = b.path("<p>.zig"), … })`
|
||||
* and `const m = b.createModule(.{ .root_source_file = … })` — a module,
|
||||
* bound to the identifier a preceding `const m =` names;
|
||||
* - `b.addExecutable` / `addLibrary` / `addStaticLibrary` /
|
||||
* `addSharedLibrary` / `addTest` / `addObject(.{ .root_source_file =
|
||||
* b.path("<p>.zig"), … })` — an artifact whose ROOT MODULE is a module of
|
||||
* its own (reached as `exe.root_module.addImport(…)`), or `.root_module =
|
||||
* m` / `.root_module = b.createModule(…)` naming one declared inline;
|
||||
* - `<m>.addImport("<alias>", <operand>)`, `<exe>.root_module.addImport(…)`
|
||||
* and the `.imports = &.{ .{ .name = "<alias>", .module = <operand> } }`
|
||||
* field of a module's own arguments — an entry in THAT module's table.
|
||||
* The operand is a module binding (`m`) or a path dep's named module,
|
||||
* `dep.module("<name>")` with `const dep = b.dependency("<zon name>", …)`,
|
||||
* looked up in `depModules` (zon dep name → module name → repo-relative
|
||||
* root, from the dep's own build.zig).
|
||||
*
|
||||
* Why per module rather than one map (`parseZigRootModules`): an alias is
|
||||
* scoped to the module that declares it. Two executables that each
|
||||
* `addImport("config", …)` their own `config.zig` are the ordinary
|
||||
* multi-target layout, and a single first-wins map sent the second module's
|
||||
* `@import("config")` to the first module's file — a confident wrong
|
||||
* `IMPORTS` edge and every `config.*` call behind it. Deliberately NOT
|
||||
* resolved, as in `parseZigRootModules`: generated roots
|
||||
* (`addOptions().createModule()`, `translate_c.createModule()`, computed
|
||||
* LazyPaths), `.url` deps, and operands that are not a bare identifier or a
|
||||
* `dep.module("…")` on a `b.dependency` binding. Comments stripped, string
|
||||
* literals masked; the first binding of an identifier wins.
|
||||
*/
|
||||
export function parseZigBuildModules(
|
||||
buildZig: string,
|
||||
depModules?: ReadonlyMap<string, ReadonlyMap<string, string>>,
|
||||
): ZigBuildModule[] {
|
||||
const text = stripZonComments(buildZig);
|
||||
const mask = zonStringMask(text);
|
||||
|
||||
// Pass 1 — modules and the identifiers bound to them. `at` is the offset
|
||||
// of the call's name token, so an inline `.root_module = b.createModule(…)`
|
||||
// can be matched back to the module it minted.
|
||||
interface Draft {
|
||||
readonly name?: string;
|
||||
readonly root: string;
|
||||
readonly at: number;
|
||||
readonly argsStart: number;
|
||||
readonly argsEnd: number;
|
||||
readonly imports: Map<string, string>;
|
||||
}
|
||||
const drafts: Draft[] = [];
|
||||
const bindings = new Map<string, number>(); // identifier → drafts index
|
||||
const bind = (prefixEnd: number, idx: number): void => {
|
||||
const binding = ZIG_MODULE_BINDING_RE.exec(text.slice(0, prefixEnd));
|
||||
if (binding && !bindings.has(binding[1]!)) bindings.set(binding[1]!, idx);
|
||||
};
|
||||
// Artifact bindings whose `.root_module = <ident>` names a module declared
|
||||
// by another call; resolved once every binding is known.
|
||||
const pendingArtifactAliases: { readonly ident: string; readonly module: string }[] = [];
|
||||
const callRe =
|
||||
/\b(addModule|createModule|addExecutable|addLibrary|addStaticLibrary|addSharedLibrary|addTest|addObject)\s*\(/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = callRe.exec(text)) !== null) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
const argsStart = m.index + m[0].length;
|
||||
const argsEnd = findZigParenEnd(text, argsStart);
|
||||
if (argsEnd < 0) break;
|
||||
const args = text.slice(argsStart, argsEnd);
|
||||
const kind = m[1]!;
|
||||
if (kind === 'addModule' || kind === 'createModule') {
|
||||
const root = zigTopLevelStaticRoot(args);
|
||||
if (root === null) continue;
|
||||
const nameMatch = kind === 'addModule' ? /^\s*"([^"\n]+)"\s*,/.exec(args) : null;
|
||||
drafts.push({
|
||||
...(nameMatch ? { name: nameMatch[1]! } : {}),
|
||||
root,
|
||||
at: m.index,
|
||||
argsStart,
|
||||
argsEnd,
|
||||
imports: new Map(),
|
||||
});
|
||||
bind(m.index, drafts.length - 1);
|
||||
continue;
|
||||
}
|
||||
// An artifact. Its root module is either declared inline by
|
||||
// `.root_source_file`, or handed over through `.root_module = …`.
|
||||
const rootModule = /\.root_module\s*=\s*((?:[A-Za-z_]\w*\.)*)([A-Za-z_]\w*)\s*(\()?/.exec(args);
|
||||
if (rootModule) {
|
||||
if (rootModule[3] === '(' && rootModule[2] === 'createModule') {
|
||||
// Inline `.root_module = b.createModule(.{ … })`: the module is minted
|
||||
// by the createModule call inside these args (a later iteration of
|
||||
// this loop); remember the artifact's binding for it.
|
||||
const nameOffset = rootModule.index + rootModule[0].lastIndexOf('createModule');
|
||||
const binding = ZIG_MODULE_BINDING_RE.exec(text.slice(0, m.index));
|
||||
if (binding) {
|
||||
pendingArtifactAliases.push({
|
||||
ident: binding[1]!,
|
||||
module: `@${argsStart + nameOffset}`,
|
||||
});
|
||||
}
|
||||
} else if (rootModule[1] === '' && rootModule[3] === undefined) {
|
||||
const binding = ZIG_MODULE_BINDING_RE.exec(text.slice(0, m.index));
|
||||
if (binding) pendingArtifactAliases.push({ ident: binding[1]!, module: rootModule[2]! });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const root = zigTopLevelStaticRoot(args);
|
||||
if (root === null) continue;
|
||||
drafts.push({ root, at: m.index, argsStart, argsEnd, imports: new Map() });
|
||||
bind(m.index, drafts.length - 1);
|
||||
}
|
||||
for (const alias of pendingArtifactAliases) {
|
||||
if (bindings.has(alias.ident)) continue;
|
||||
const idx = alias.module.startsWith('@')
|
||||
? drafts.findIndex((d) => d.at === Number(alias.module.slice(1)))
|
||||
: (bindings.get(alias.module) ?? -1);
|
||||
if (idx >= 0) bindings.set(alias.ident, idx);
|
||||
}
|
||||
if (drafts.length === 0) return [];
|
||||
|
||||
// `const dep = b.dependency("<zon name>", …)` bindings, for `dep.module("…")`.
|
||||
const dependencyBindings = new Map<string, string>();
|
||||
const depRe =
|
||||
/(?:const|var)\s+([A-Za-z_]\w*)\s*=\s*(?:[A-Za-z_]\w*\.)*dependency\(\s*"([^"\n]+)"/g;
|
||||
while ((m = depRe.exec(text)) !== null) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
if (!dependencyBindings.has(m[1]!)) dependencyBindings.set(m[1]!, m[2]!);
|
||||
}
|
||||
// An import operand → the repo-relative root it names, or null.
|
||||
const operandRoot = (operand: string): string | null => {
|
||||
const bare = /^([A-Za-z_]\w*)$/.exec(operand);
|
||||
if (bare) {
|
||||
const idx = bindings.get(bare[1]!);
|
||||
return idx === undefined ? null : drafts[idx]!.root;
|
||||
}
|
||||
const viaDep = /^([A-Za-z_]\w*)\.module\(\s*"([^"\n]+)"\s*\)$/.exec(operand);
|
||||
if (viaDep) {
|
||||
const zonName = dependencyBindings.get(viaDep[1]!);
|
||||
return zonName === undefined ? null : (depModules?.get(zonName)?.get(viaDep[2]!) ?? null);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const addImport = (idx: number, alias: string, operand: string): void => {
|
||||
const root = operandRoot(operand.trim());
|
||||
const table = drafts[idx]!.imports;
|
||||
if (root !== null && !table.has(alias)) table.set(alias, root);
|
||||
};
|
||||
|
||||
// Pass 2a — `<m>.addImport("<alias>", <operand>)` / `<exe>.root_module.addImport(…)`.
|
||||
const addImportRe = /\b([A-Za-z_]\w*)(?:\.root_module)?\.addImport\s*\(/g;
|
||||
while ((m = addImportRe.exec(text)) !== null) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
const idx = bindings.get(m[1]!);
|
||||
if (idx === undefined) continue;
|
||||
const argsStart = m.index + m[0].length;
|
||||
const argsEnd = findZigParenEnd(text, argsStart);
|
||||
if (argsEnd < 0) break;
|
||||
const args = text.slice(argsStart, argsEnd);
|
||||
const aliasMatch = /^\s*"([^"\n]+)"\s*,/.exec(args);
|
||||
if (!aliasMatch) continue;
|
||||
addImport(idx, aliasMatch[1]!, args.slice(aliasMatch[0].length));
|
||||
}
|
||||
// Pass 2b — `.imports = &.{ .{ .name = "<alias>", .module = <operand> }, … }`
|
||||
// inside a module's own argument list. The operand runs to the next `,` or
|
||||
// `}` at paren depth 0 (`dep.module("core")` carries parentheses).
|
||||
const entryRe = /\.name\s*=\s*"([^"\n]+)"\s*,\s*\.module\s*=\s*/g;
|
||||
drafts.forEach((draft, idx) => {
|
||||
const args = text.slice(draft.argsStart, draft.argsEnd);
|
||||
let e: RegExpExecArray | null;
|
||||
while ((e = entryRe.exec(args)) !== null) {
|
||||
if (mask[draft.argsStart + e.index] !== 0) continue;
|
||||
let depth = 0;
|
||||
let end = e.index + e[0].length;
|
||||
for (; end < args.length; end++) {
|
||||
const ch = args[end];
|
||||
if (ch === '(') depth++;
|
||||
else if (ch === ')') {
|
||||
if (depth === 0) break;
|
||||
depth--;
|
||||
} else if (depth === 0 && (ch === ',' || ch === '}')) break;
|
||||
}
|
||||
addImport(idx, e[1]!, args.slice(e.index + e[0].length, end));
|
||||
}
|
||||
});
|
||||
|
||||
return drafts.map(({ name, root, imports }) => ({
|
||||
...(name !== undefined ? { name } : {}),
|
||||
root,
|
||||
imports,
|
||||
}));
|
||||
}
|
||||
|
||||
/** First `.root_source_file = b.path("….zig")` at the TOP level of a
|
||||
* module-options `.{ … }` — not a nested `.imports = &.{ .{ … } }` entry. */
|
||||
function zigTopLevelStaticRoot(args: string): string | null {
|
||||
const mask = zonStringMask(args);
|
||||
let structAt = -1;
|
||||
for (let i = 0; i < args.length - 1; i++) {
|
||||
if (mask[i] !== 0) continue;
|
||||
if (args[i] === '.' && args[i + 1] === '{') {
|
||||
structAt = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (structAt < 0) return null;
|
||||
const bodyStart = structAt + 2;
|
||||
const bodyEnd = findZonBlockEnd(args, bodyStart);
|
||||
if (bodyEnd < 0) return null;
|
||||
const match = /\.root_source_file\s*=\s*b\.path\(\s*"([^"\n]+)"\s*\)/.exec(
|
||||
zonBlankNestedBlocks(args.slice(bodyStart, bodyEnd)),
|
||||
);
|
||||
if (match === null) return null;
|
||||
const root = normalizeZigDepPath(match[1]!);
|
||||
return root === null || root === '' || !root.endsWith('.zig') ? null : root;
|
||||
}
|
||||
|
||||
/** `const m = b.createModule` / `const m = b.addModule` — not `config.createModule`. */
|
||||
const ZIG_MODULE_BINDING_RE = /(?:const|var)\s+([A-Za-z_]\w*)\s*=\s*b\.$/;
|
||||
|
||||
/**
|
||||
* Index of the `)` matching the `(` that precedes `start`, skipping parens
|
||||
* inside `"…"` literals. -1 when unbalanced. Call on comment-stripped text.
|
||||
*/
|
||||
function findZigParenEnd(text: string, start: number): number {
|
||||
let depth = 1;
|
||||
let inString = false;
|
||||
for (let i = start; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (inString) {
|
||||
if (ch === '\\') i++;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') inString = true;
|
||||
else if (ch === '(') depth++;
|
||||
else if (ch === ')' && --depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blank out `//` line comments (and `\\` multiline-string-literal lines) in
|
||||
* ZON source, string-aware: a `//` inside a `"…"` literal (`.url =
|
||||
* "https://…"`) is content, not a comment. Comment bytes are replaced with
|
||||
* spaces so every surviving character keeps its offset.
|
||||
*/
|
||||
function stripZonComments(raw: string): string {
|
||||
const out = raw.split('');
|
||||
let inString = false;
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const ch = raw[i];
|
||||
if (inString) {
|
||||
if (ch === '\\')
|
||||
i++; // skip the escaped char
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
const isLineComment = ch === '/' && raw[i + 1] === '/';
|
||||
const isMultilineLiteral =
|
||||
ch === '\\' &&
|
||||
raw[i + 1] === '\\' &&
|
||||
/^[ \t]*$/.test(raw.slice(raw.lastIndexOf('\n', i) + 1, i));
|
||||
if (isLineComment || isMultilineLiteral) {
|
||||
while (i < raw.length && raw[i] !== '\n') out[i++] = ' ';
|
||||
}
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Index of the `}` matching the `{` that precedes `start`, skipping braces
|
||||
* inside `"…"` literals. -1 when unbalanced. Call on comment-stripped text.
|
||||
*/
|
||||
function findZonBlockEnd(text: string, start: number): number {
|
||||
let depth = 1;
|
||||
let inString = false;
|
||||
for (let i = start; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (inString) {
|
||||
if (ch === '\\') i++;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') inString = true;
|
||||
else if (ch === '{') depth++;
|
||||
else if (ch === '}' && --depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* `body` with every nested `{ … }` block (string-aware) replaced by spaces of
|
||||
* equal length, so a regex over the result only sees the block's DIRECT
|
||||
* fields and offsets still line up with the original text.
|
||||
*/
|
||||
function zonBlankNestedBlocks(body: string): string {
|
||||
const out = body.split('');
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const ch = body[i];
|
||||
if (inString) {
|
||||
if (ch === '\\') {
|
||||
if (depth > 0 && i + 1 < body.length) out[i + 1] = ' ';
|
||||
i++;
|
||||
} else if (ch === '"') inString = false;
|
||||
if (depth > 0) out[i] = ' ';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') inString = true;
|
||||
else if (ch === '{') depth++;
|
||||
else if (ch === '}' && depth > 0) {
|
||||
depth--;
|
||||
out[i] = ' ';
|
||||
continue;
|
||||
}
|
||||
if (depth > 0) out[i] = ' ';
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-offset "is inside a `"…"` literal" mask for comment-stripped ZON text,
|
||||
* so header regexes can reject a match that merely LOOKS like a field
|
||||
* (`.name = ".dependencies = .{ … }"` is a string, not the dependencies
|
||||
* block). Escaped quotes (`\"`) do not end the literal.
|
||||
*/
|
||||
function zonStringMask(text: string): Uint8Array {
|
||||
const mask = new Uint8Array(text.length);
|
||||
let inString = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (inString) {
|
||||
mask[i] = 1;
|
||||
if (ch === '\\' && i + 1 < text.length) mask[++i] = 1;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inString = true;
|
||||
mask[i] = 1;
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-offset brace depth for comment-stripped ZON text, string-aware: the
|
||||
* depth AT an offset is the number of unclosed `{` before it. The file's
|
||||
* top-level `.{` puts every direct field at depth 1.
|
||||
*/
|
||||
function zonDepthMask(text: string): Uint8Array {
|
||||
const depth = new Uint8Array(text.length);
|
||||
let d = 0;
|
||||
let inString = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
depth[i] = d;
|
||||
if (inString) {
|
||||
if (ch === '\\' && i + 1 < text.length) depth[++i] = d;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') inString = true;
|
||||
else if (ch === '{') d++;
|
||||
else if (ch === '}' && d > 0) d--;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* First match of a sticky-free global `re` in `text[from, to)` whose start
|
||||
* lies outside a string literal (per `mask`) and, when `depthAt` is given, at
|
||||
* exactly that brace depth (per `depth`). Null when none.
|
||||
*/
|
||||
function matchZonHeader(
|
||||
text: string,
|
||||
re: RegExp,
|
||||
mask: Uint8Array,
|
||||
from: number,
|
||||
to: number,
|
||||
depth?: Uint8Array,
|
||||
depthAt?: number,
|
||||
): RegExpExecArray | null {
|
||||
re.lastIndex = from;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(text)) !== null && m.index < to) {
|
||||
if (mask[m.index] !== 0) continue;
|
||||
if (depth !== undefined && depthAt !== undefined && depth[m.index] !== depthAt) continue;
|
||||
return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Pure parser split out for testability. Returns null when no path-deps found. */
|
||||
export function parseZigBuildZon(raw: string): ZigBuildZonConfig | null {
|
||||
const text = stripZonComments(raw);
|
||||
const mask = zonStringMask(text);
|
||||
const depth = zonDepthMask(text);
|
||||
// Locate the `.dependencies = .{ ... }` block. Use brace counting because
|
||||
// dep entries are nested anonymous structs and a naive `}` match would stop
|
||||
// early — and only accept a header outside string literals AND at brace
|
||||
// depth 1 (a direct field of the file's top-level `.{`), so neither a
|
||||
// `.name` value spelling `.dependencies = .{` nor a `.dependencies` field
|
||||
// nested in some earlier anonymous struct can hijack it.
|
||||
const depsHeader = matchZonHeader(
|
||||
text,
|
||||
/\.dependencies\s*=\s*\.\{/g,
|
||||
mask,
|
||||
0,
|
||||
text.length,
|
||||
depth,
|
||||
1,
|
||||
);
|
||||
if (!depsHeader) return null;
|
||||
const start = depsHeader.index + depsHeader[0].length;
|
||||
const end = findZonBlockEnd(text, start);
|
||||
if (end < 0) return null;
|
||||
|
||||
const pathDeps = new Map<string, string>();
|
||||
// Walk each `.<name> = .{ ... }` entry inside [start, end); the body ends
|
||||
// at the matching brace (string-aware), not at the first `}` in the text,
|
||||
// and an entry header inside a string (`.url = "…/.x = .{"`) is not an entry.
|
||||
const entryHeaderRe = /\.([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\.\{/g;
|
||||
let cursor = start;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = matchZonHeader(text, entryHeaderRe, mask, cursor, end)) !== null) {
|
||||
const depName = m[1];
|
||||
const bodyStart = m.index + m[0].length;
|
||||
const bodyEnd = findZonBlockEnd(text, bodyStart);
|
||||
if (bodyEnd < 0 || bodyEnd > end) break;
|
||||
cursor = bodyEnd + 1;
|
||||
// Only a `.path` that is a DIRECT field of the entry counts: a nested
|
||||
// object inside the entry (`.foo = .{ .url = "…", .x = .{ .path = "…" } }`)
|
||||
// must not turn a URL dep into a path dep. Blank nested blocks first and
|
||||
// reject a match that starts inside a string literal.
|
||||
const body = zonBlankNestedBlocks(text.slice(bodyStart, bodyEnd));
|
||||
const pathMatch = matchZonHeader(
|
||||
body,
|
||||
/\.path\s*=\s*"([^"\n]+)"/g,
|
||||
mask.subarray(bodyStart, bodyEnd),
|
||||
0,
|
||||
body.length,
|
||||
);
|
||||
if (pathMatch) {
|
||||
pathDeps.set(depName, pathMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathDeps.size === 0) return null;
|
||||
if (isDev) {
|
||||
logger.info(`📦 Loaded ${pathDeps.size} Zig path-dep(s) from build.zig.zon`);
|
||||
}
|
||||
return { pathDeps };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BUNDLED CONFIG LOADER
|
||||
// ============================================================================
|
||||
|
|
@ -571,6 +1331,9 @@ export interface ImportConfigs {
|
|||
csharpConfigs: CSharpProjectConfig[];
|
||||
/** In-repo namespace evidence gating C# suffix-fallback resolution (#1881). */
|
||||
csharpNamespaces?: CSharpNamespaceEvidence;
|
||||
/** Zig `.path` deps from build.zig.zon. Optional so call sites that
|
||||
* hand-build ImportConfigs (tests) don't have to supply it. */
|
||||
zigBuildZon?: ZigBuildZonConfig | null;
|
||||
}
|
||||
|
||||
/** Load all language-specific configs once for an ingestion run. */
|
||||
|
|
@ -583,5 +1346,6 @@ export async function loadImportConfigs(repoRoot: string): Promise<ImportConfigs
|
|||
swiftPackageConfig: await loadSwiftPackageConfig(repoRoot),
|
||||
csharpConfigs: csharpScan.configs,
|
||||
csharpNamespaces: csharpScanToEvidence(csharpScan),
|
||||
zigBuildZon: await loadZigBuildConfig(repoRoot),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,43 @@ interface LanguageProviderConfig {
|
|||
* Default: undefined (no remapping). */
|
||||
readonly resolveEnclosingOwner?: (node: SyntaxNode) => SyntaxNode | null;
|
||||
|
||||
/**
|
||||
* The type a whole FILE declares, when the language makes the file itself
|
||||
* a type (Zig: a `.zig` file with top-level fields is a struct whose name
|
||||
* is the file stem — `Page.zig` declares `Page`, and `page.getArena()`
|
||||
* dispatches onto the file's top-level `fn getArena(self: *Page)`).
|
||||
*
|
||||
* Consulted by the enclosing-owner walk when it reaches the tree root
|
||||
* without meeting a container, and by the class/method/field extractors
|
||||
* for the owner name. Return `null` for a file that is only a namespace.
|
||||
* The name is the class-like node's name (`Struct:<file>:<name>`), so the
|
||||
* owner id and the node id agree by construction.
|
||||
* Default: undefined (a file never owns members). */
|
||||
readonly resolveFileTypeOwner?: (
|
||||
root: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null;
|
||||
|
||||
/**
|
||||
* The type a CONTAINER node declares, when the language names it from its
|
||||
* context rather than from a name child of the node — a binding wrapper,
|
||||
* an enclosing callable, an ordinal among anonymous siblings (Zig:
|
||||
* `const T = struct {…}` is `T`; a function-local `const R = struct {…}`
|
||||
* inside `fn string` is `string$R`; `struct { fn lessThan … }.lessThan`
|
||||
* passed to a sort is `<fn>$1`).
|
||||
*
|
||||
* Consulted by the enclosing-owner walk for every `CLASS_CONTAINER_TYPES`
|
||||
* node it meets (after `resolveEnclosingOwner` remapping), BEFORE the
|
||||
* generic name-child derivation; return `null` to fall back to it. The name
|
||||
* must be the one the class-like node is minted under
|
||||
* (`<label>:<file>:<name>`), so a member's owner id and the node id agree
|
||||
* by construction.
|
||||
* Default: undefined (containers are named by the generic derivation). */
|
||||
readonly resolveContainerTypeOwner?: (
|
||||
container: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null;
|
||||
|
||||
// ── Enclosing function resolution ───────────────────────────────
|
||||
/** Resolve the enclosing function name + label from an AST ancestor node
|
||||
* that is NOT a standard FUNCTION_NODE_TYPE. For languages where the
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { swiftProvider } from './swift.js';
|
|||
import { dartProvider } from './dart.js';
|
||||
import { vueProvider } from './vue.js';
|
||||
import { cobolProvider } from './cobol.js';
|
||||
import { zigProvider } from './zig.js';
|
||||
|
||||
export const providers = {
|
||||
[SupportedLanguages.JavaScript]: javascriptProvider,
|
||||
|
|
@ -43,6 +44,7 @@ export const providers = {
|
|||
[SupportedLanguages.Dart]: dartProvider,
|
||||
[SupportedLanguages.Vue]: vueProvider,
|
||||
[SupportedLanguages.Cobol]: cobolProvider,
|
||||
[SupportedLanguages.Zig]: zigProvider,
|
||||
} satisfies Record<SupportedLanguages, LanguageProvider>;
|
||||
|
||||
/** Get provider by language enum (always succeeds for SupportedLanguages). */
|
||||
|
|
|
|||
|
|
@ -36,14 +36,14 @@ const RUST_SCOPE_QUERY = `
|
|||
type_parameters: (type_parameters)? @declaration.type-parameters) @declaration.enum
|
||||
|
||||
;; Declarations — union
|
||||
;; Deliberately tagged @declaration.struct (→ Struct label), NOT a
|
||||
;; @declaration.union: every registry-primary resolution gate —
|
||||
;; isLinkableLabel (node-lookup.ts), CALLABLE_OR_TYPE_LIKE
|
||||
;; (finalize-algorithm.ts), ClassLikeNodeLabel (class-types.ts) — includes
|
||||
;; Struct but EXCLUDES Union, so a Union-labeled node would be an
|
||||
;; unresolvable orphan. A Rust union is a type whose literal is a real
|
||||
;; constructor, so Struct is both the resolvable and the semantically
|
||||
;; honest label here. #1934 F71.
|
||||
;; Tagged @declaration.struct (→ Struct label), NOT @declaration.union.
|
||||
;; Historically forced: the registry-primary gates (isLinkableLabel,
|
||||
;; CALLABLE_OR_TYPE_LIKE, ClassLikeNodeLabel) excluded Union, so a
|
||||
;; Union-labeled node was an unresolvable orphan (#1934 F71). Zig support
|
||||
;; widened all three for its union(enum) containers, so the label is now
|
||||
;; resolvable — Struct is kept here on the semantic argument alone (a Rust
|
||||
;; union is a type whose literal is a real constructor) and to leave the
|
||||
;; graph node ids of existing Rust indexes unchanged.
|
||||
(union_item
|
||||
name: (type_identifier) @declaration.name
|
||||
type_parameters: (type_parameters)? @declaration.type-parameters) @declaration.struct
|
||||
|
|
|
|||
145
gitnexus/src/core/ingestion/languages/zig.ts
Normal file
145
gitnexus/src/core/ingestion/languages/zig.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* Zig Language Provider.
|
||||
*
|
||||
* Key Zig traits:
|
||||
* - mroStrategy: default 'first-wins' is irrelevant — Zig has no inheritance,
|
||||
* and no heritage hooks are provided (Zig queries never produce
|
||||
* `@heritage.*` captures).
|
||||
* - exportChecker: walks to the enclosing variable_declaration /
|
||||
* function_declaration and looks for a `pub` or `export` keyword child.
|
||||
* - importResolver: resolves local `@import("./foo.zig")` paths and
|
||||
* build.zig.zon `.path` deps (root the dep's build.zig declares, then
|
||||
* src/root.zig, src/<name>.zig, src/main.zig); `@import("std")` and
|
||||
* `.url` packages are deliberately external.
|
||||
* - namedBindingExtractor: omitted — the scope side handles
|
||||
* `const Foo = @import("x").Foo` and `const Foo = ns.Foo` (ns an
|
||||
* @import binding) as NAMED imports instead (`zig/captures.ts`).
|
||||
* - scope-resolution hooks (Ring 3): `emitScopeCaptures` walks the file via
|
||||
* `zig/query.ts` (containers as Class scopes — including the container a
|
||||
* generic type constructor `fn List(comptime T: type) type` returns,
|
||||
* named after the fn; container-nested fns relabeled @declaration.method;
|
||||
* plain-variable groups filtered for container/import bindings and for
|
||||
* the keyword-less `variable_declaration`s tree-sitter-zig uses for
|
||||
* statement assignments); `interpretImport` maps `const x = @import("…")`
|
||||
* 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, `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).
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import { ZIG_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { zigExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { zigImportConfig } from '../import-resolvers/configs/zig.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { zigCallConfig } from '../call-extractors/configs/zig.js';
|
||||
import { createClassExtractor } from '../class-extractors/generic.js';
|
||||
import { zigClassConfig } from '../class-extractors/configs/zig.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { zigFieldConfig } from '../field-extractors/configs/zig.js';
|
||||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
import { zigMethodConfig } from '../method-extractors/configs/zig.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { zigVariableConfig } from '../variable-extractors/configs/zig.js';
|
||||
import { zigTypeConfig } from '../type-extractors/zig.js';
|
||||
import {
|
||||
emitZigScopeCaptures,
|
||||
interpretZigImport,
|
||||
interpretZigTypeBinding,
|
||||
isZigContainerMethod,
|
||||
isZigFileStruct,
|
||||
isZigRedundantContainerCapture,
|
||||
isZigTypeShadowingBinding,
|
||||
zigArityCompatibility,
|
||||
zigContainerLabel,
|
||||
zigContainerName,
|
||||
zigFileStructName,
|
||||
zigBindingScopeFor,
|
||||
zigReceiverBinding,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
} from './zig/index.js';
|
||||
|
||||
export const zigProvider = defineLanguage({
|
||||
id: SupportedLanguages.Zig,
|
||||
extensions: ['.zig'],
|
||||
entryPointPatterns: [
|
||||
/^main$/, // standard executable entry point
|
||||
/^build$/, // build.zig entry point
|
||||
],
|
||||
astFrameworkPatterns: [],
|
||||
treeSitterQueries: ZIG_QUERIES,
|
||||
typeConfig: zigTypeConfig,
|
||||
exportChecker: zigExportChecker,
|
||||
importResolver: createImportResolver(zigImportConfig),
|
||||
callExtractor: createCallExtractor(zigCallConfig),
|
||||
classExtractor: createClassExtractor(zigClassConfig),
|
||||
fieldExtractor: createFieldExtractor(zigFieldConfig),
|
||||
methodExtractor: createMethodExtractor(zigMethodConfig),
|
||||
variableExtractor: createVariableExtractor(zigVariableConfig),
|
||||
// A `const`/`var` whose value is a container or an `@import` is the
|
||||
// Struct/Enum/Union node or the import binding, not a Const beside it.
|
||||
// Up to three ZIG_QUERIES rules match one container (wrapper, type
|
||||
// constructor, bare container — F8); `zigContainerAnchor` names the one
|
||||
// that mints it and the others are dropped here.
|
||||
shouldSkipDefinitionCapture: (captureMap, defaultLabel) => {
|
||||
if (defaultLabel === 'Const' || defaultLabel === 'Variable') {
|
||||
const decl = captureMap['definition.const'] ?? captureMap['definition.variable'];
|
||||
return decl !== undefined && isZigTypeShadowingBinding(decl);
|
||||
}
|
||||
if (defaultLabel === 'Struct' || defaultLabel === 'Enum' || defaultLabel === 'Union') {
|
||||
const decl =
|
||||
captureMap['definition.struct'] ??
|
||||
captureMap['definition.enum'] ??
|
||||
captureMap['definition.union'];
|
||||
if (decl === undefined) return false;
|
||||
// The file-struct rules over-match (a `@This` first parameter, a
|
||||
// top-level `@This()` alias — see ZIG_QUERIES); the one predicate decides.
|
||||
if (decl.type === 'source_file') return !isZigFileStruct(decl);
|
||||
return isZigRedundantContainerCapture(decl, captureMap['name']);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// A file whose top level declares fields IS a struct named after the file
|
||||
// (`Page.zig` → `Page`): its top-level fns/fields are members of that
|
||||
// Struct. Files without fields are namespaces and own nothing.
|
||||
resolveFileTypeOwner: (root, filePath) =>
|
||||
isZigFileStruct(root) ? { name: zigFileStructName(filePath), label: 'Struct' } : null,
|
||||
// Every container's identity comes from `zigContainerName` — the binding
|
||||
// name for `const T = struct {…}` at file/container level, the fn name for
|
||||
// a generic type constructor, and (F8) `string$R` / `build$1` for
|
||||
// function-local and anonymous containers, which no name child spells. The
|
||||
// class extractor names the node from the same function, so a member's
|
||||
// owner id (`Method:<file>:string$R.get`) and the node id agree.
|
||||
resolveContainerTypeOwner: (container, filePath) => {
|
||||
if (!ZIG_CONTAINER_TYPES.has(container.type)) return null;
|
||||
const name = zigContainerName(container, filePath);
|
||||
const label = zigContainerLabel(container);
|
||||
return name !== undefined && label !== undefined ? { name, label } : null;
|
||||
},
|
||||
labelOverride: (functionNode, defaultLabel) => {
|
||||
if (defaultLabel !== 'Function') return defaultLabel;
|
||||
if (isZigContainerMethod(functionNode)) return 'Method';
|
||||
return defaultLabel;
|
||||
},
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
||||
emitScopeCaptures: emitZigScopeCaptures,
|
||||
interpretImport: interpretZigImport,
|
||||
// `@import` is compile-time name lookup, not an executed statement: one
|
||||
// written inside a function body is resolved exactly as one at file scope
|
||||
// (same answer as C `#include` and Rust `use`). Without this, a
|
||||
// function-scoped `@import` would be marked `runsOnlyWhenCalled` and a
|
||||
// real import cycle through it would be hidden from `check --cycles`.
|
||||
importsExecuteWhereWritten: false,
|
||||
interpretTypeBinding: interpretZigTypeBinding,
|
||||
bindingScopeFor: zigBindingScopeFor,
|
||||
receiverBinding: zigReceiverBinding,
|
||||
// Provider contract is (def, callsite); the ScopeResolver contract is
|
||||
// (callsite, def) — same function, adapted argument order.
|
||||
arityCompatibility: (def, callsite) => zigArityCompatibility(callsite, def),
|
||||
});
|
||||
2175
gitnexus/src/core/ingestion/languages/zig/captures.ts
Normal file
2175
gitnexus/src/core/ingestion/languages/zig/captures.ts
Normal file
File diff suppressed because it is too large
Load diff
36
gitnexus/src/core/ingestion/languages/zig/index.ts
Normal file
36
gitnexus/src/core/ingestion/languages/zig/index.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
export {
|
||||
emitZigScopeCaptures,
|
||||
isZigContainerMethod,
|
||||
isZigContainerOrImportBinding,
|
||||
isZigFileStruct,
|
||||
isZigFileThisAlias,
|
||||
isZigKeywordDeclaration,
|
||||
isZigRedundantContainerCapture,
|
||||
isZigTypeShadowingBinding,
|
||||
zigCallableQualifiedName,
|
||||
zigContainerAnchor,
|
||||
zigContainerBindingName,
|
||||
zigContainerLabel,
|
||||
zigContainerName,
|
||||
zigFileStructName,
|
||||
zigImportRootOf,
|
||||
zigCallReturnTypeOf,
|
||||
zigReturnTypeIsNominal,
|
||||
zigTypeConstructorOf,
|
||||
zigUnwrapValue,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
} from './captures.js';
|
||||
export {
|
||||
populateZigRangeBindings,
|
||||
zigElementSpelling,
|
||||
zigOptionalPayloadSpelling,
|
||||
zigPointeeSpelling,
|
||||
} from './range-binding.js';
|
||||
export { interpretZigImport, interpretZigTypeBinding, normalizeZigTypeName } from './interpret.js';
|
||||
export {
|
||||
expandZigWildcardNames,
|
||||
zigArityCompatibility,
|
||||
zigBindingScopeFor,
|
||||
zigMergeBindings,
|
||||
zigReceiverBinding,
|
||||
} from './simple-hooks.js';
|
||||
176
gitnexus/src/core/ingestion/languages/zig/interpret.ts
Normal file
176
gitnexus/src/core/ingestion/languages/zig/interpret.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
|
||||
|
||||
const stripQuotes = (s: string): string => s.replace(/^["']|["']$/g, '');
|
||||
|
||||
/**
|
||||
* The module a `@import` target names, for the `importedName` of a namespace
|
||||
* import (the shared contract wants the MODULE there — Go's `import foo
|
||||
* "pkg/bar"` records `bar` — not the local handle): the last path segment
|
||||
* without its `.zig` extension. `"std"` → `std`, `"./net/socket.zig"` →
|
||||
* `socket`, `"mylib"` (a build.zig.zon dep) → `mylib`.
|
||||
*/
|
||||
export function zigModuleNameOf(targetRaw: string): string {
|
||||
const last = targetRaw.replace(/\\/g, '/').split('/').filter(Boolean).pop() ?? targetRaw;
|
||||
return last.endsWith('.zig') ? last.slice(0, -'.zig'.length) : last;
|
||||
}
|
||||
|
||||
/**
|
||||
* `const std = @import("std");` binds the imported module to a const handle
|
||||
* accessed via qualified syntax — a namespace import (closest peers: Python
|
||||
* `import numpy`, Go `import "pkg/bar"`). `localName` is the handle the
|
||||
* author chose, `importedName` the module (`zigModuleNameOf`).
|
||||
*
|
||||
* `_ = @import("x.zig");` (and any keyword-less `<ident> = @import(…)`, a
|
||||
* statement rather than a declaration in this grammar) references the file
|
||||
* without binding a name — the `refAllDecls` / test-aggregation idiom. That
|
||||
* is a `side-effect` import: file edge, no binding (TS `import './x'`). So is
|
||||
* an `@import` in any expression position (a tuple element, a call argument,
|
||||
* a comparison operand — `emitZigScopeCaptures`'s `@import.inline` rule).
|
||||
*
|
||||
* The receiver of a member call, `@import("dump.zig").root(...)`, arrives as
|
||||
* a namespace import whose `@import.name` is the builtin's own text: the
|
||||
* shared namespace-receiver lookup keys on the receiver text, and that is
|
||||
* how the call resolves into `dump.zig` without a `const` handle.
|
||||
*/
|
||||
export function interpretZigImport(captures: CaptureMatch): ParsedImport | null {
|
||||
const source = captures['@import.source']?.text;
|
||||
if (source === undefined) return null;
|
||||
const targetRaw = stripQuotes(source);
|
||||
if (targetRaw.length === 0) return null;
|
||||
|
||||
// `pub usingnamespace @import("x.zig");` — every pub decl of the target
|
||||
// becomes a decl of this container. A wildcard, expanded by
|
||||
// `expandZigWildcardNames` in the scope resolver.
|
||||
if (captures['@import.wildcard'] !== undefined) {
|
||||
return { kind: 'wildcard', targetRaw };
|
||||
}
|
||||
if (captures['@import.side-effect'] !== undefined) {
|
||||
return { kind: 'side-effect', targetRaw };
|
||||
}
|
||||
|
||||
const name = captures['@import.name']?.text;
|
||||
if (name === undefined) return null;
|
||||
|
||||
// `const Foo = @import("x.zig").Foo;` — one member, under a name of the
|
||||
// importer's choosing (a rename when it differs: `const Alloc =
|
||||
// @import("std").mem;`). Same fact as TS `import { Foo } from './x'` /
|
||||
// `import { Foo as Bar }`.
|
||||
const imported = captures['@import.imported']?.text;
|
||||
if (imported !== undefined) {
|
||||
// `pub const X = …` at file scope republishes the name (Python
|
||||
// `__init__.py` shape): a third file reads it as `thisModule.X`. The
|
||||
// marker is set by `emitZigScopeCaptures` where the syntax node is
|
||||
// still available (`isZigPublishingImport`).
|
||||
const republish = captures['@import.reexports'] !== undefined ? { reexportsName: true } : {};
|
||||
return imported === name
|
||||
? { kind: 'named', localName: name, importedName: imported, targetRaw, ...republish }
|
||||
: {
|
||||
kind: 'alias',
|
||||
localName: name,
|
||||
importedName: imported,
|
||||
alias: name,
|
||||
targetRaw,
|
||||
...republish,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'namespace',
|
||||
localName: name,
|
||||
importedName: zigModuleNameOf(targetRaw),
|
||||
targetRaw,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip Zig type sigils that wrap the nominal type: pointers (`*T`, `[*]T`),
|
||||
* optionals (`?T`), error unions (`!T` / `E!T`), slices (`[]T`), arrays
|
||||
* (`[N]T`), and `const` qualifiers. Keeps the bare type name so registry
|
||||
* lookup matches the container declaration.
|
||||
*/
|
||||
export function normalizeZigTypeName(text: string): string {
|
||||
let t = text.trim();
|
||||
// Error union first: the payload of `E!*T` / `!?T` carries its own sigils
|
||||
// (F6: `Allocator.Error!*Page` is the shape of every fallible constructor).
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
let previous: string;
|
||||
do {
|
||||
previous = t;
|
||||
t = t.replace(/^(\*|\?|\[\*?c?\]|\[[^\]]*\])\s*/, '');
|
||||
t = t.replace(/^const\s+/, '');
|
||||
} while (t !== previous);
|
||||
// Generic instantiation `List(u8)` / `std.ArrayList(u8)` → the type
|
||||
// constructor `List` / `std.ArrayList`: Zig spells a generic type as a
|
||||
// call, and the container def is registered under the function's name.
|
||||
// Builtins (`@This()`, `@TypeOf(x)`) keep their parentheses — they are
|
||||
// not constructor names and must not turn into `@This`.
|
||||
if (!t.startsWith('@')) {
|
||||
const paren = t.indexOf('(');
|
||||
if (paren > 0 && t.endsWith(')')) t = t.slice(0, paren).trim();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
export function interpretZigTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null {
|
||||
const name = captures['@type-binding.name']?.text;
|
||||
const type = captures['@type-binding.type']?.text;
|
||||
if (name === undefined || type === undefined) return null;
|
||||
|
||||
let source: TypeRef['source'] = 'annotation';
|
||||
if (captures['@type-binding.parameter'] !== undefined) {
|
||||
// Zig has no implicit receiver keyword. `emitZigScopeCaptures` tags the
|
||||
// receiver parameter (`@type-binding.receiver`, see `zigReceiverParameter`):
|
||||
// the FIRST parameter when it is named `self` OR typed as the enclosing
|
||||
// container (`counter: *Counter`, `pool: *@This()`, `prng: *PRNG` with
|
||||
// `const PRNG = @This();`). Position matters — `fn f(a: u32, self: T)` is
|
||||
// legal and `self` there is an ordinary parameter.
|
||||
const isReceiver = captures['@type-binding.receiver'] !== undefined;
|
||||
source = isReceiver ? 'self' : 'parameter-annotation';
|
||||
} else if (captures['@type-binding.constructor'] !== undefined) {
|
||||
source = 'constructor-inferred';
|
||||
} else if (captures['@type-binding.call-return'] !== undefined) {
|
||||
// `var c = Counter.init();` — the call's receiver names the type when it
|
||||
// is a container (`Counter`, `mod.Counter`, `List(u8)`); a value receiver
|
||||
// (`std.mem`, `self.items`) simply finds no container and declines.
|
||||
// `const t = makeThing();` — the callee name, chained to its return
|
||||
// binding by the shared resolver.
|
||||
source = 'constructor-inferred';
|
||||
// `const el = node.asElement();` on a fn-LOCAL receiver (F6): the type is
|
||||
// the METHOD's return type, spelled as the compound `node.asElement()` the
|
||||
// shared resolver walks (receiver binding → class scope → the method's
|
||||
// `@type-binding.return`). Kept verbatim: `normalizeZigTypeName` would
|
||||
// read the `()` as a generic instantiation and strip it.
|
||||
if (captures['@type-binding.member-call-return'] !== undefined) {
|
||||
return { boundName: name, rawTypeName: type.trim(), source };
|
||||
}
|
||||
} else if (captures['@type-binding.return'] !== undefined) {
|
||||
// `fn make() !*Thing` — `make ↦ Thing`, in the enclosing scope, so a
|
||||
// call site chains through it. Error unions / pointers / optionals are
|
||||
// stripped by `normalizeZigTypeName` (`Allocator.Error!*Page` → `Page`).
|
||||
source = 'return-annotation';
|
||||
} 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) {
|
||||
// Two alias shapes share the group: `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) — and
|
||||
// `const LocalAlias = Local;` / `const B = util.List(u8);` — the alias
|
||||
// name is bound to the value's type text (`util.List` after the comptime
|
||||
// arguments are dropped) and chained to the target by the shared
|
||||
// `followChainedRef` (F7). Rust's `let x = y` source: it must rank
|
||||
// BELOW an annotation on the same name (`const x: T = y;` is typed by
|
||||
// `T`), and the default here is 'annotation'.
|
||||
source = 'assignment-inferred';
|
||||
}
|
||||
|
||||
return { boundName: name, rawTypeName: normalizeZigTypeName(type), source };
|
||||
}
|
||||
432
gitnexus/src/core/ingestion/languages/zig/query.ts
Normal file
432
gitnexus/src/core/ingestion/languages/zig/query.ts
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
import Parser from 'tree-sitter';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Zig scope-resolution query (RFC #909 Ring 3).
|
||||
*
|
||||
* The grammar is an optionalDependency (`@tree-sitter-grammars/tree-sitter-zig`),
|
||||
* so the language module is required lazily and `getZigParser` /
|
||||
* `getZigScopeQuery` throw only when actually invoked without the grammar
|
||||
* installed. That is safe: the parse pipeline filters `.zig` files through
|
||||
* `parser-loader.isLanguageAvailable` before any scope extraction runs.
|
||||
*
|
||||
* Zig specifics encoded here:
|
||||
* - Containers (struct/enum/union/opaque) are anonymous nodes bound by the
|
||||
* enclosing `variable_declaration`; declarations capture the binding
|
||||
* identifier from the wrapper.
|
||||
* - `@import` is a builtin call, not import-statement syntax; the
|
||||
* `#eq?` predicate keeps other builtins (@sizeOf, @as, …) out.
|
||||
* - A plain `(variable_declaration (identifier))` rule would also match
|
||||
* container and import bindings — `emitZigScopeCaptures` filters those
|
||||
* groups out so a name binds exactly once.
|
||||
*/
|
||||
const ZIG_SCOPE_QUERY = `
|
||||
;; Scopes
|
||||
(source_file) @scope.module
|
||||
(struct_declaration) @scope.class
|
||||
(enum_declaration) @scope.class
|
||||
(union_declaration) @scope.class
|
||||
(opaque_declaration) @scope.class
|
||||
(function_declaration) @scope.function
|
||||
(test_declaration) @scope.function
|
||||
(block) @scope.block
|
||||
|
||||
;; Declarations — functions (relabeled @declaration.method inside containers
|
||||
;; by emitZigScopeCaptures, mirroring the provider's labelOverride)
|
||||
(function_declaration
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
;; Declarations — named tests. Same naming rule as ZIG_QUERIES: the string
|
||||
;; node WITH quotes, so the def joins the graph node and never collides with
|
||||
;; a same-named fn. Anonymous / decl-form tests are scopes without a def.
|
||||
(test_declaration
|
||||
(string) @declaration.name) @declaration.function
|
||||
|
||||
;; Declarations — containers. The binding name lives on the wrapper
|
||||
;; variable_declaration, but the ANCHOR is the container node itself so its
|
||||
;; range equals the @scope.class range: the extractor then attaches the def
|
||||
;; to the class scope (walkers.populateClassOwnedMembers expects the
|
||||
;; class-like def among the class scope's ownedDefs) and auto-hoists the
|
||||
;; name binding to the parent scope. Keyword-gated like the ordinary
|
||||
;; binding rules: a keyword-less \`x = struct {…};\` is an assignment
|
||||
;; (tree-sitter-zig 1.1.2 reuses variable_declaration), not a container def.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @declaration.name
|
||||
(struct_declaration) @declaration.struct)
|
||||
(variable_declaration
|
||||
"var" . (identifier) @declaration.name
|
||||
(struct_declaration) @declaration.struct)
|
||||
(variable_declaration
|
||||
"const" . (identifier) @declaration.name
|
||||
(enum_declaration) @declaration.enum)
|
||||
(variable_declaration
|
||||
"var" . (identifier) @declaration.name
|
||||
(enum_declaration) @declaration.enum)
|
||||
(variable_declaration
|
||||
"const" . (identifier) @declaration.name
|
||||
(union_declaration) @declaration.union)
|
||||
(variable_declaration
|
||||
"var" . (identifier) @declaration.name
|
||||
(union_declaration) @declaration.union)
|
||||
;; opaque {} is a fieldless container that may own methods — Struct, as in
|
||||
;; ZIG_QUERIES (see the rationale there).
|
||||
(variable_declaration
|
||||
"const" . (identifier) @declaration.name
|
||||
(opaque_declaration) @declaration.struct)
|
||||
(variable_declaration
|
||||
"var" . (identifier) @declaration.name
|
||||
(opaque_declaration) @declaration.struct)
|
||||
|
||||
;; Declarations — generic type constructors. \`fn List(comptime T: type) type
|
||||
;; { return struct {…}; }\` is Zig's only spelling of a generic type; the
|
||||
;; returned container is anonymous in the grammar but every reader (and every
|
||||
;; caller: \`List(u8)\`) names it after the function. Anchor on the container
|
||||
;; so the def sits in its own class scope; the name binding is hoisted to the
|
||||
;; MODULE scope by \`zigBindingScopeFor\` (not the fn body, where the name
|
||||
;; would be invisible to callers) and coexists with the Function def of the
|
||||
;; same name — \`List\` really is both a callable and a type.
|
||||
((function_declaration
|
||||
name: (identifier) @declaration.name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(struct_declaration) @declaration.struct))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @declaration.name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(union_declaration) @declaration.union))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @declaration.name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(enum_declaration) @declaration.enum))))
|
||||
(#eq? @_ret "type"))
|
||||
|
||||
;; 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). 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
|
||||
type: (_)? @declaration.field-type) @declaration.field
|
||||
(#not-eq? @declaration.name ""))
|
||||
|
||||
;; Declarations — const/var bindings (import/container groups filtered in TS).
|
||||
;; The \`.\` anchor pins the FIRST named child: without it the pattern also
|
||||
;; matched the initializer of \`const first = target;\`, minting a phantom
|
||||
;; local named \`target\` that shadowed the real callee for every later
|
||||
;; reference in the block. The literal keyword is load-bearing too:
|
||||
;; tree-sitter-zig 1.1.2 parses statement assignments (\`x = 5;\`, \`x += 1;\`,
|
||||
;; \`_ = expr;\`) as \`variable_declaration\` WITHOUT a keyword child, and
|
||||
;; without the keyword every assignment and every discard minted a phantom
|
||||
;; local (one \`_\` per statement).
|
||||
(variable_declaration
|
||||
"const" . (identifier) @declaration.name) @declaration.variable
|
||||
(variable_declaration
|
||||
"var" . (identifier) @declaration.name) @declaration.variable
|
||||
|
||||
;; Imports — const x = @import("...") / var x = @import("..."). Keyword-gated
|
||||
;; like every binding rule: a keyword-less \`x = @import("...")\` is a
|
||||
;; statement (see the side-effect rule below), not a binding.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @import.name
|
||||
(builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
(variable_declaration
|
||||
"var" . (identifier) @import.name
|
||||
(builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
|
||||
;; Imports — const X = @import("...").X : a NAMED import of one member. The
|
||||
;; local name is whatever the user chose (\`const Alloc = @import("std").mem;\`
|
||||
;; is a rename), the imported name is the member. A deeper chain
|
||||
;; (\`@import("std").mem.Allocator\`, \`@import("lib.zig").B.work\`) is matched
|
||||
;; by the second rule below but is NOT bound as a named import of the
|
||||
;; innermost member: that discarded the written owner (\`B\`) and let a
|
||||
;; same-named \`A.work\` answer first. \`emitZigScopeCaptures\` binds the module
|
||||
;; under the builtin's text instead and rewrites the alias's use sites to the
|
||||
;; full path (\`collectZigDeepAliases\`, PR #1432 review 8.4).
|
||||
(variable_declaration
|
||||
"const" . (identifier) @import.name
|
||||
(field_expression
|
||||
object: (builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
member: (identifier) @import.imported)
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
(variable_declaration
|
||||
"const" . (identifier) @import.name
|
||||
(field_expression
|
||||
object: (field_expression
|
||||
object: (builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source)))
|
||||
member: (identifier) @import.imported)
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
|
||||
;; Imports — a keyword-less \`<ident> = @import("...");\` statement
|
||||
;; (\`_ = @import("all_tests.zig");\` in a test block, the refAllDecls
|
||||
;; idiom): tree-sitter-zig reuses \`variable_declaration\` for assignments, so
|
||||
;; the shape is a declaration minus the keyword. It references the file
|
||||
;; without binding a name — a side-effect import. Tree-sitter queries cannot
|
||||
;; say "no keyword child", so this rule matches the keyword-bearing shapes
|
||||
;; too; \`emitZigScopeCaptures\` keeps it only when \`isZigKeywordDeclaration\`
|
||||
;; is false (the keyword shapes are the binding rules above).
|
||||
(variable_declaration
|
||||
. (identifier)
|
||||
(builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.side-effect
|
||||
|
||||
;; Aliases of a namespace member — const Counter = counter.Counter; where
|
||||
;; \`counter\` is an @import binding of THIS file. The query cannot know which
|
||||
;; identifiers are import bindings, so it captures every one-level member
|
||||
;; alias and \`emitZigScopeCaptures\` promotes the ones whose object is a
|
||||
;; known @import to a named import (same fact as \`const Counter =
|
||||
;; @import("counter.zig").Counter;\`); the rest stay ordinary variables.
|
||||
;; Only the ONE-level shape is promoted; a deeper chain (\`lib.B.work\`,
|
||||
;; \`std.mem.Allocator\`) is a deep alias — a Const whose use sites are
|
||||
;; rewritten to the written owner path (see the import rule above, 8.4).
|
||||
(variable_declaration
|
||||
"const" . (identifier) @alias.name
|
||||
(field_expression
|
||||
object: (identifier) @alias.namespace
|
||||
member: (identifier) @alias.member) .) @alias.statement
|
||||
(variable_declaration
|
||||
"const" . (identifier) @alias.name
|
||||
(field_expression
|
||||
object: (field_expression
|
||||
object: (identifier) @alias.namespace)
|
||||
member: (identifier) @alias.member) .) @alias.statement
|
||||
|
||||
;; Imports — pub usingnamespace @import("..."); : every pub decl of the target
|
||||
;; becomes a decl of this container (removed from the language in 0.15, still
|
||||
;; everywhere in 0.11–0.14 code). Modelled as a wildcard import.
|
||||
(using_namespace_declaration
|
||||
(builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.wildcard
|
||||
|
||||
;; Imports — \`@import("...")\` in ANY other position: a tuple element
|
||||
;; (\`pub const Interfaces = .{ @import("a.zig"), @import("b.zig") }\`, the
|
||||
;; JS-API registration table), a call argument (\`event.is(@import("x.zig"))\`),
|
||||
;; a comparison operand (\`T == @import("x.zig").T\`), the receiver of a
|
||||
;; member call (\`try @import("dump.zig").root(...)\`), a 3-deep member chain…
|
||||
;; Every one of them is a file dependency; only the const/var/usingnamespace
|
||||
;; shapes above bind a name. This rule matches EVERY \`@import\` builtin, the
|
||||
;; bound shapes included — \`emitZigScopeCaptures\` drops the matches whose
|
||||
;; string node a binding rule (or the keyword-less side-effect rule) already
|
||||
;; claimed, so a bound import is never doubled, and emits the rest as
|
||||
;; side-effect imports (file edge, no binding) — except the member-call
|
||||
;; receiver, which becomes a namespace import keyed by its own source text so
|
||||
;; the call resolves into the imported module (see the emitter).
|
||||
((builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.inline
|
||||
|
||||
;; Type bindings — parameter annotations (incl. self: *T receivers)
|
||||
(parameter
|
||||
name: (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.parameter
|
||||
|
||||
;; Type bindings — constructor inference: const p = T{ ... }. Keyword-gated
|
||||
;; like every binding rule: a keyword-less \`p = T{ ... };\` is a
|
||||
;; re-assignment (same node type in tree-sitter-zig 1.1.2), and Zig's static
|
||||
;; typing means \`p\` already carries its type from its declaration
|
||||
;; (annotation, constructor or inferred value) — the assignment declares
|
||||
;; nothing, and \`_ = T{ ... };\` must not bind \`_\`.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(identifier) @type-binding.type)) @type-binding.constructor
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — qualified constructor: const p = mod.T{ ... }. The whole
|
||||
;; field_expression is captured so the dotted text "mod.T" survives —
|
||||
;; receiver dispatch resolves the namespace prefix through the import
|
||||
;; binding (emitReceiverBoundCalls Case 3).
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(field_expression) @type-binding.type)) @type-binding.constructor
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(field_expression) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — generic instantiation literal: const l = List(u8){ ... }.
|
||||
;; The callee is the type constructor; \`normalizeZigTypeName\` drops the
|
||||
;; comptime argument list so \`List(u8)\` looks up \`List\`.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(call_expression) @type-binding.type)) @type-binding.constructor
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(call_expression) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — declared type: var x: T = …; const x: T = .init(…);
|
||||
;; The annotation is the ONLY type source for \`= undefined\` and for 0.14+
|
||||
;; decl literals (\`.init\`, \`.empty\`), which are the idiomatic
|
||||
;; constructors in current std. Ranked below constructor inference by the
|
||||
;; shared resolver (source 'annotation'), so a literal on the right still
|
||||
;; wins when both are present.
|
||||
(variable_declaration
|
||||
. (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.annotation
|
||||
|
||||
;; Type bindings — value inference (F6): const t = <value>; where <value> is a
|
||||
;; call (\`Counter.init()\`, \`makeThing()\`, \`node.asElement()\`), possibly
|
||||
;; wrapped in \`try\` / \`catch …\` / \`… orelse …\` / parentheses — the shape of
|
||||
;; nearly every Zig constructor call (\`const p = try Page.init(…)\`). The
|
||||
;; query only pins the declaration; \`emitZigScopeCaptures\` unwraps the
|
||||
;; wrappers and rewrites \`@type-binding.type\` to the type source (see
|
||||
;; \`zigCallReturnTypeOf\`). Keyword-gated: \`_ = e.top();\` is an assignment
|
||||
;; (same node type), not a binding of \`_\`. Rust's twin is
|
||||
;; \`let x = Foo::new()\` / \`let x = foo().await\`.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(_) @type-binding.value .) @type-binding.call-return
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(_) @type-binding.value .) @type-binding.call-return
|
||||
|
||||
;; Type bindings — return-type annotation (F6): \`fn make() !*Thing\` binds
|
||||
;; \`make ↦ Thing\` in the enclosing scope (Module for free fns, the container's
|
||||
;; Class scope for methods — that is where the compound resolver reads a
|
||||
;; method's return type for \`node.asElement()\`), so \`const t = makeThing()\`
|
||||
;; chains to \`Thing\`. Rust: \`(function_item … return_type:) @type-binding.return\`.
|
||||
;; \`emitZigScopeCaptures\` drops builtin / \`type\` returns (a \`List ↦ type\`
|
||||
;; binding would hijack the \`List(u8){}\` constructor chain).
|
||||
(function_declaration
|
||||
name: (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.return
|
||||
|
||||
;; Type bindings — aliases (F5 field-access aliases + F7 type aliases):
|
||||
;; \`const page = self.page;\` (F5: the RHS path is kept verbatim as the
|
||||
;; "type", the compound resolver's member-alias branch re-resolves it as a
|
||||
;; receiver chain — head \`self\` → class → field type), \`const LocalAlias = Local;\`,
|
||||
;; \`const Proto = HtmlElement;\`, \`const T2 = Thing;\` (alias of an alias /
|
||||
;; import), \`const B = util.List(u8);\` (an INSTANTIATED generic type
|
||||
;; constructor). Zig has no \`type X = Y\` syntax — a type alias is a const
|
||||
;; whose value is a type expression, and it stays a Const in the graph. What
|
||||
;; must change is the scope side: bind the alias NAME to the value's type
|
||||
;; text (Rust's \`let x = y\` / JS's \`const B = Foo\` \`@type-binding.alias\`,
|
||||
;; source 'assignment-inferred'), so \`LocalAlias.mk()\` types through Case 4,
|
||||
;; \`B.init()\` / \`x: B\` / \`B{}\` through Case 3 once \`normalizeZigTypeName\`
|
||||
;; drops the comptime arguments (\`util.List(u8)\` → \`util.List\`), and every
|
||||
;; binding that names the alias (\`var l = LocalAlias.mk()\`, \`var x: B\`) is
|
||||
;; chained to the target by the shared \`followChainedRef\` /
|
||||
;; \`followChainPostFinalize\`. The identifier / member shapes take \`var\` too:
|
||||
;; \`var node = orig_node;\` is the same value alias as Rust's \`let x = y\`
|
||||
;; and chains to the type of \`orig_node\` (a Zig type is comptime and never
|
||||
;; \`var\`, so the type-alias reading only ever applies to \`const\`). The
|
||||
;; call shape is \`const\`-only and kept only when the callee's last
|
||||
;; identifier is TitleCase — see \`emitZigScopeCaptures\` (a value call
|
||||
;; \`const t = util.makeThing()\` belongs to the call-return rules above and
|
||||
;; must not receive a competing binding). A promoted namespace-member alias
|
||||
;; (\`const Counter = counter.Counter;\` → named import) is skipped there too:
|
||||
;; the import binding already carries the type.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(identifier) @type-binding.type .) @type-binding.alias
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(identifier) @type-binding.type .) @type-binding.alias
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(field_expression) @type-binding.type .) @type-binding.alias
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(field_expression) @type-binding.type .) @type-binding.alias
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(call_expression) @type-binding.type .) @type-binding.alias
|
||||
|
||||
;; References — free calls: foo(...)
|
||||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; References — member calls: obj.method(...) / mod.fn(...)
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @reference.receiver
|
||||
member: (identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; References — constructor uses: T{ ... }
|
||||
(struct_initializer
|
||||
(identifier) @reference.name) @reference.call.constructor
|
||||
|
||||
;; References — qualified constructor uses: mod.T{ ... } / hub.sub.T{ ... }
|
||||
;; Captured with the RECEIVER, not as a free constructor with a raw qualified
|
||||
;; name, on purpose: the free-call fallback resolves a qualified constructor by
|
||||
;; its simple tail, and a workspace-unique \`Thing\` then answers for
|
||||
;; \`other.Thing{}\` whichever module the source named (measured: \`c.Thing{}\`
|
||||
;; with no \`Thing\` in c.zig bound to a.zig's). With the receiver the site goes
|
||||
;; through the receiver-bound namespace case, which resolves the member inside
|
||||
;; the module the receiver is bound to — the same path \`mod.fn()\` takes — so
|
||||
;; \`a.Thing{}\` and \`b.Thing{}\` each bind their own file and \`std.Thread.Mutex{}\`
|
||||
;; binds nothing even when a local \`Mutex\` exists.
|
||||
(struct_initializer
|
||||
(field_expression
|
||||
object: (_) @reference.receiver
|
||||
member: (identifier) @reference.name)) @reference.call.constructor
|
||||
|
||||
;; References — generic instantiation literals: List(u8){ ... } /
|
||||
;; lists.List(u8){ ... }. The type head is a call_expression — the
|
||||
;; instantiation of the type constructor — which neither constructor rule
|
||||
;; above matches, so the OUTER aggregate event had no site: only the inner
|
||||
;; \`List(u8)\` call (a free / member call reference on the call node) reached
|
||||
;; the graph (PR #1432 review, 8.11). Two sites on two anchors: the call
|
||||
;; (an invocation of \`List\`) and this initializer (a construction of the
|
||||
;; container \`List\` returns, marked \`(constructor)\`). The receiver form goes
|
||||
;; through the same namespace path as \`mod.T{}\`.
|
||||
(struct_initializer
|
||||
(call_expression
|
||||
function: (identifier) @reference.name)) @reference.call.constructor
|
||||
(struct_initializer
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @reference.receiver
|
||||
member: (identifier) @reference.name))) @reference.call.constructor
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
let _query: Parser.Query | null = null;
|
||||
|
||||
function getZigLanguage(): Parameters<Parser['setLanguage']>[0] {
|
||||
return _require('@tree-sitter-grammars/tree-sitter-zig');
|
||||
}
|
||||
|
||||
export function getZigParser(): Parser {
|
||||
if (_parser === null) {
|
||||
_parser = new Parser();
|
||||
_parser.setLanguage(getZigLanguage());
|
||||
}
|
||||
return _parser;
|
||||
}
|
||||
|
||||
export function getZigScopeQuery(): Parser.Query {
|
||||
if (_query === null) {
|
||||
_query = new Parser.Query(getZigLanguage(), ZIG_SCOPE_QUERY);
|
||||
}
|
||||
return _query;
|
||||
}
|
||||
375
gitnexus/src/core/ingestion/languages/zig/range-binding.ts
Normal file
375
gitnexus/src/core/ingestion/languages/zig/range-binding.ts
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
/**
|
||||
* Zig payload captures (F6): `for (items) |it|`, `if (opt) |v|`,
|
||||
* `while (it.next()) |x|`. The captured name has no annotation anywhere — its
|
||||
* type is the SUBJECT's type minus one layer (slice/array element, optional
|
||||
* payload) — so the tree-sitter query cannot bind it; this post-finalize hook
|
||||
* reads the subject's binding from the finished scope model and injects the
|
||||
* payload's. Rust/Go do the same (`rust/range-binding.ts`,
|
||||
* `go/range-binding.ts`).
|
||||
*
|
||||
* Deliberately narrow and honest: a payload binds only when the subject's
|
||||
* WRITTEN type visibly has the layer the construct removes — `[]T` / `[N]T` /
|
||||
* `[*]T` / `*[N]T` for `for`, `?T` (after any `E!`) for `if` / `while`. A
|
||||
* subject typed `std.ArrayList(T)`, `anytype`, or anything unresolved binds
|
||||
* nothing rather than guessing. `catch |err|` (an error, never a container
|
||||
* receiver) and `switch` prongs are skipped. The same one-layer projection
|
||||
* types `const t = items[i];` / `opt.?` / `ptr.*` (`bindProjectedLocal`).
|
||||
*
|
||||
* Runs after `propagateImportedReturnTypes`, so return bindings hoisted from
|
||||
* other files are visible when a subject is a call.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, Scope, ScopeId, TypeRef } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { getZigParser } from './query.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe, ParseTimeoutError } from '../../../tree-sitter/safe-parse.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { logger } from '../../../logger.js';
|
||||
import {
|
||||
findClassBindingInScope,
|
||||
findReceiverTypeBinding,
|
||||
isClassLike,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import { isZigKeywordDeclaration, zigUnwrapValue } from './captures.js';
|
||||
import { normalizeZigTypeName } from './interpret.js';
|
||||
|
||||
type ZigTree = ReturnType<ReturnType<typeof getZigParser>['parse']>;
|
||||
|
||||
const PAYLOAD_HOSTS: ReadonlySet<string> = new Set([
|
||||
'for_statement',
|
||||
'for_expression',
|
||||
'if_statement',
|
||||
'if_expression',
|
||||
'while_statement',
|
||||
'while_expression',
|
||||
]);
|
||||
|
||||
export function populateZigRangeBindings(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
indexes: ScopeResolutionIndexes,
|
||||
ctx: {
|
||||
readonly fileContents: ReadonlyMap<string, string>;
|
||||
readonly treeCache?: { get(filePath: string): unknown };
|
||||
},
|
||||
): void {
|
||||
const parser = getZigParser();
|
||||
|
||||
// Class def nodeId → its Class scope (whose typeBindings hold the members'
|
||||
// field types and the methods' return types). Same derivation as
|
||||
// `buildWorkspaceResolutionIndex`, which this hook does not receive.
|
||||
const classScopeByDefId = new Map<string, Scope>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Class') continue;
|
||||
const cd = scope.ownedDefs.find((d) => isClassLike(d.type));
|
||||
if (cd !== undefined) classScopeByDefId.set(cd.nodeId, scope);
|
||||
}
|
||||
}
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const sourceText = ctx.fileContents.get(parsed.filePath);
|
||||
if (sourceText === undefined) continue;
|
||||
let tree = ctx.treeCache?.get(parsed.filePath) as ZigTree | undefined;
|
||||
if (tree === undefined) {
|
||||
try {
|
||||
tree = parseSourceSafe(parser, sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ParseTimeoutError) {
|
||||
logger.warn(
|
||||
{ file: parsed.filePath },
|
||||
'zig range-binding: parse timed out, skipping file',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const scopes = parsed.scopes;
|
||||
if (scopes.length === 0) continue;
|
||||
const resolver = new ZigSubjectTypeResolver(scopes, indexes, classScopeByDefId);
|
||||
|
||||
// Pre-order: an outer payload is bound before an inner construct reads it
|
||||
// (`for (pages) |p| { if (p.frame) |f| … }`).
|
||||
const visit = (node: SyntaxNode): void => {
|
||||
if (PAYLOAD_HOSTS.has(node.type)) bindPayloads(node, resolver);
|
||||
else if (node.type === 'variable_declaration') bindProjectedLocal(node, resolver);
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c !== null) visit(c);
|
||||
}
|
||||
};
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
/** Bind the payload identifiers of one `for` / `if` / `while` node. */
|
||||
function bindPayloads(node: SyntaxNode, resolver: ZigSubjectTypeResolver): void {
|
||||
const named = node.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
// The payload right after the subject(s); a trailing `else |err|` payload
|
||||
// belongs to the else_clause and is not a child here.
|
||||
const payload = named.find((c) => c.type === 'payload');
|
||||
if (payload === undefined) return;
|
||||
const isFor = node.type === 'for_statement' || node.type === 'for_expression';
|
||||
// A `for` lists its subjects BEFORE the payload; the body follows it.
|
||||
const subjects: SyntaxNode[] = isFor
|
||||
? named.slice(0, named.indexOf(payload)).filter((c) => c.type !== 'block_label')
|
||||
: [node.childForFieldName('condition')].filter((c): c is SyntaxNode => c !== null);
|
||||
if (subjects.length === 0) return;
|
||||
// For a `for` the payload names pair positionally with the subjects
|
||||
// (`for (items, 0..) |it, i|`); `if` / `while` capture one value.
|
||||
const captured = payload.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type === 'identifier',
|
||||
);
|
||||
const host = payloadHostScope(node, resolver);
|
||||
if (host === undefined) return;
|
||||
for (let i = 0; i < captured.length && i < subjects.length; i++) {
|
||||
const name = captured[i]!.text;
|
||||
if (name === '_') continue;
|
||||
const subject = subjects[i]!;
|
||||
if (subject.type === 'range_expression') continue; // `0..` — an index
|
||||
const spelling = resolver.spellingOf(subject);
|
||||
if (spelling === undefined) continue;
|
||||
const projected = isFor ? zigElementSpelling(spelling) : zigOptionalPayloadSpelling(spelling);
|
||||
if (projected === undefined) continue;
|
||||
// `|*p|` captures a POINTER to the element/payload (the `*` is an
|
||||
// anonymous payload child right before the identifier). Keep the written
|
||||
// `*` so a later deref projection (`const q = p.*;`) still sees the
|
||||
// layer; `rawName` strips it again, so method dispatch is unchanged.
|
||||
const element = captured[i]!.previousSibling?.type === '*' ? `*${projected}` : projected;
|
||||
const rawName = normalizeZigTypeName(element);
|
||||
if (rawName.length === 0 || rawName.startsWith('@')) continue;
|
||||
const existing = host.typeBindings.get(name);
|
||||
// Zig forbids shadowing, so an existing binding of this name in the host
|
||||
// can only be a sibling construct's payload (bare-expression bodies share
|
||||
// the enclosing scope). Two different types for one name: decline both
|
||||
// rather than let the last one type the first one's body.
|
||||
if (existing !== undefined) {
|
||||
if (existing.rawName !== rawName) {
|
||||
(host.typeBindings as Map<string, TypeRef>).delete(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const ref: TypeRef =
|
||||
element === rawName
|
||||
? { rawName, declaredAtScope: host.id, source: 'annotation' }
|
||||
: { rawName, declaredSpelling: element, declaredAtScope: host.id, source: 'annotation' };
|
||||
(host.typeBindings as Map<string, TypeRef>).set(name, ref);
|
||||
}
|
||||
}
|
||||
|
||||
/** `const t = items[i];` / `const t = opt.?;` / `const t = ptr.*;` — a local
|
||||
* bound to ONE LAYER under a typed subject: the element of a slice/array
|
||||
* (not a `[a..b]` re-slice), the payload of an optional, the pointee of a
|
||||
* pointer. Same honesty rule as the payloads: the subject's WRITTEN type
|
||||
* must show the layer. Skipped when the name is already typed (an
|
||||
* annotation, or a capture-time inference). */
|
||||
function bindProjectedLocal(decl: SyntaxNode, resolver: ZigSubjectTypeResolver): void {
|
||||
if (!isZigKeywordDeclaration(decl)) return;
|
||||
const named = decl.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
if (named.length < 2 || named[0]!.type !== 'identifier') return;
|
||||
const value = named[named.length - 1]!;
|
||||
if (value.id === decl.childForFieldName('type')?.id) return; // `const x: T;`
|
||||
const inner = zigUnwrapValue(value);
|
||||
let subject: SyntaxNode | null;
|
||||
let project: (spelling: string) => string | undefined;
|
||||
switch (inner.type) {
|
||||
case 'index_expression':
|
||||
if (inner.childForFieldName('index')?.type === 'range_expression') return; // `a[0..n]`
|
||||
subject = inner.childForFieldName('object');
|
||||
project = zigElementSpelling;
|
||||
break;
|
||||
case 'null_coercion_expression':
|
||||
subject = inner.namedChild(0);
|
||||
project = zigOptionalPayloadSpelling;
|
||||
break;
|
||||
case 'dereference_expression':
|
||||
subject = inner.namedChild(0);
|
||||
project = zigPointeeSpelling;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (subject === null) return;
|
||||
const name = named[0]!.text;
|
||||
const host = resolver.scopeAt(decl.startPosition.row + 1, decl.startPosition.column, false);
|
||||
if (host === undefined || host.typeBindings.has(name)) return;
|
||||
const spelling = resolver.spellingOf(subject);
|
||||
if (spelling === undefined) return;
|
||||
const projected = project(spelling);
|
||||
if (projected === undefined) return;
|
||||
const rawName = normalizeZigTypeName(projected);
|
||||
if (rawName.length === 0 || rawName.startsWith('@')) return;
|
||||
const ref: TypeRef =
|
||||
projected === rawName
|
||||
? { rawName, declaredAtScope: host.id, source: 'assignment-inferred' }
|
||||
: {
|
||||
rawName,
|
||||
declaredSpelling: projected,
|
||||
declaredAtScope: host.id,
|
||||
source: 'assignment-inferred',
|
||||
};
|
||||
(host.typeBindings as Map<string, TypeRef>).set(name, ref);
|
||||
}
|
||||
|
||||
/** `*T` / `*const T` / `E!*T` → `T`; undefined when not visibly a pointer. */
|
||||
export function zigPointeeSpelling(spelling: string): string | undefined {
|
||||
let t = spelling.trim();
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
const m = /^\*(const\s+)?/.exec(t);
|
||||
if (m === null) return undefined;
|
||||
const rest = t.slice(m[0].length).trim();
|
||||
return rest.length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
function body(node: SyntaxNode): SyntaxNode | null {
|
||||
return node.childForFieldName('body');
|
||||
}
|
||||
|
||||
/** The scope the payload name lives in: the body block's own scope when the
|
||||
* body is a block (`|t| { … }`), otherwise the innermost scope enclosing the
|
||||
* construct (a bare-expression body: `for (items) |t| t.run();`). */
|
||||
function payloadHostScope(node: SyntaxNode, resolver: ZigSubjectTypeResolver): Scope | undefined {
|
||||
// for/if/while_statement carry `body:`; in the *_expression forms the
|
||||
// body is the named child right after the payload.
|
||||
let bodyNode = body(node);
|
||||
if (bodyNode === null) {
|
||||
const named = node.namedChildren.filter((c): c is SyntaxNode => c !== null);
|
||||
const at = named.findIndex((c) => c.type === 'payload');
|
||||
bodyNode = at === -1 ? null : (named[at + 1] ?? null);
|
||||
}
|
||||
let block: SyntaxNode | null = bodyNode;
|
||||
if (block?.type === 'block_expression')
|
||||
block = block.namedChildren.find((c) => c?.type === 'block') ?? null;
|
||||
if (block?.type === 'labeled_statement')
|
||||
block = block.namedChildren.find((c) => c?.type === 'block') ?? null;
|
||||
if (block?.type === 'block') {
|
||||
const exact = resolver.scopeAt(block.startPosition.row + 1, block.startPosition.column, true);
|
||||
if (exact !== undefined) return exact;
|
||||
}
|
||||
return resolver.scopeAt(node.startPosition.row + 1, node.startPosition.column, false);
|
||||
}
|
||||
|
||||
/** `[]T` / `[]const T` / `[N]T` / `[*]T` / `[*:0]T` / `*[N]T` / `*const []T`
|
||||
* → `T` (with `T`'s own sigils kept: `[]*Thing` → `*Thing`). Undefined when
|
||||
* the written type has no slice/array layer to remove. */
|
||||
export function zigElementSpelling(spelling: string): string | undefined {
|
||||
let t = spelling.trim();
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
// A pointer TO an array/slice iterates the pointee.
|
||||
t = t.replace(/^\*(const\s+)?(?=\[)/, '');
|
||||
const m = /^\[[^\]]*\]\s*(const\s+)?/.exec(t);
|
||||
if (m === null) return undefined;
|
||||
const rest = t.slice(m[0].length).trim();
|
||||
return rest.length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
/** `?T` / `?*T` / `E!?T` → `T` (sigils of `T` kept). Undefined when the
|
||||
* written type is not visibly optional. */
|
||||
export function zigOptionalPayloadSpelling(spelling: string): string | undefined {
|
||||
let t = spelling.trim();
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
if (!t.startsWith('?')) return undefined;
|
||||
const rest = t.slice(1).trim();
|
||||
return rest.length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
/** Resolves a subject expression to the WRITTEN type spelling of its binding
|
||||
* (`declaredSpelling` when the capture layer reduced it, else `rawName`),
|
||||
* through the finished scope model of one file. */
|
||||
class ZigSubjectTypeResolver {
|
||||
constructor(
|
||||
private readonly scopes: readonly Scope[],
|
||||
private readonly indexes: ScopeResolutionIndexes,
|
||||
private readonly classScopeByDefId: ReadonlyMap<string, Scope>,
|
||||
) {}
|
||||
|
||||
/** Innermost scope containing (line, col); with `exact`, only a scope
|
||||
* STARTING there (a block's own scope). */
|
||||
scopeAt(line: number, col: number, exact: boolean): Scope | undefined {
|
||||
let best: Scope | undefined;
|
||||
for (const s of this.scopes) {
|
||||
const r = s.range;
|
||||
if (exact) {
|
||||
if (r.startLine === line && r.startCol === col && s.kind === 'Block') return s;
|
||||
continue;
|
||||
}
|
||||
const startsBefore = r.startLine < line || (r.startLine === line && r.startCol <= col);
|
||||
const endsAfter = r.endLine > line || (r.endLine === line && r.endCol >= col);
|
||||
if (!startsBefore || !endsAfter) continue;
|
||||
if (best === undefined || contains(best.range, r)) best = s;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
spellingOf(subject: SyntaxNode): string | undefined {
|
||||
const ref = this.typeRefOf(subject);
|
||||
return ref === undefined ? undefined : (ref.declaredSpelling ?? ref.rawName);
|
||||
}
|
||||
|
||||
private typeRefOf(subjectRaw: SyntaxNode): TypeRef | undefined {
|
||||
const subject = zigUnwrapValue(subjectRaw);
|
||||
const scope = this.scopeAt(subject.startPosition.row + 1, subject.startPosition.column, false);
|
||||
if (scope === undefined) return undefined;
|
||||
switch (subject.type) {
|
||||
case 'identifier':
|
||||
return findReceiverTypeBinding(scope.id, subject.text, this.indexes);
|
||||
case 'field_expression': {
|
||||
// `self.items` / `page.session` — the member's binding on the
|
||||
// object's class scope (field type, or a method's return type).
|
||||
const object = subject.childForFieldName('object');
|
||||
const member = subject.childForFieldName('member');
|
||||
if (object === null || member === null) return undefined;
|
||||
return this.memberRefOf(object, member.text, scope.id);
|
||||
}
|
||||
case 'call_expression': {
|
||||
const callee = subject.childForFieldName('function');
|
||||
if (callee === null) return undefined;
|
||||
if (callee.type === 'identifier') {
|
||||
// A free call: the fn's return binding, in the scope chain.
|
||||
return findReceiverTypeBinding(scope.id, callee.text, this.indexes);
|
||||
}
|
||||
if (callee.type !== 'field_expression') return undefined;
|
||||
const object = callee.childForFieldName('object');
|
||||
const member = callee.childForFieldName('member');
|
||||
if (object === null || member === null) return undefined;
|
||||
return this.memberRefOf(object, member.text, scope.id);
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** The binding of `member` on the class the `object` expression is typed
|
||||
* by — recursively through field chains (`self.a.b`). */
|
||||
private memberRefOf(object: SyntaxNode, member: string, scopeId: ScopeId): TypeRef | undefined {
|
||||
const objectRef = this.typeRefOf(object);
|
||||
if (objectRef === undefined) return undefined;
|
||||
// A compound rawName (`node.asElement()`, F6's member-call shape) needs
|
||||
// the shared compound resolver; not walked here.
|
||||
if (objectRef.rawName.includes('(')) return undefined;
|
||||
const classDef = findClassBindingInScope(scopeId, objectRef.rawName, this.indexes);
|
||||
if (classDef === undefined) return undefined;
|
||||
const classScope = this.classScopeByDefId.get(classDef.nodeId);
|
||||
return classScope?.typeBindings.get(member);
|
||||
}
|
||||
}
|
||||
|
||||
function contains(outer: Scope['range'], inner: Scope['range']): boolean {
|
||||
const startsBefore =
|
||||
outer.startLine < inner.startLine ||
|
||||
(outer.startLine === inner.startLine && outer.startCol <= inner.startCol);
|
||||
const endsAfter =
|
||||
outer.endLine > inner.endLine ||
|
||||
(outer.endLine === inner.endLine && outer.endCol >= inner.endCol);
|
||||
return startsBefore && endsAfter;
|
||||
}
|
||||
79
gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts
Normal file
79
gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Zig `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by the
|
||||
* generic `runScopeResolution` orchestrator.
|
||||
*
|
||||
* Thin wiring: Zig has no inheritance (default MRO linearization over an
|
||||
* empty heritage set), no `super`, and is statically typed (field-fallback
|
||||
* heuristic off per the contract guidance). Import resolution reuses the
|
||||
* same `resolveZigImportInternal` the legacy import-resolver config wraps,
|
||||
* with `build.zig.zon` `.path` deps threaded through `loadResolutionConfig`.
|
||||
*/
|
||||
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { loadZigBuildConfig, type ZigBuildZonConfig } from '../../language-config.js';
|
||||
import { resolveZigImportInternal } from '../../import-resolvers/zig.js';
|
||||
import { zigProvider } from '../zig.js';
|
||||
import { expandZigWildcardNames, zigArityCompatibility, zigMergeBindings } from './index.js';
|
||||
import { populateZigRangeBindings } from './range-binding.js';
|
||||
|
||||
export const zigScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Zig,
|
||||
languageProvider: zigProvider,
|
||||
importEdgeReason: 'zig-scope: import',
|
||||
// A struct literal `T{ .f = x }` is a CALLS edge to the Struct node (the
|
||||
// Rust `T { .. }` / Go `T{}` shape). Zig has no Constructor nodes, so
|
||||
// nothing but this marker tells that edge from an invocation on the edge
|
||||
// itself — `main → SpawnRequest` looked like a call to a function
|
||||
// (PR #1432 review). Emits `local-call (constructor)` and friends.
|
||||
markConstructionSites: true,
|
||||
// Hub modules are how Zig projects publish their types: `pub const Terminal
|
||||
// = @import("Terminal.zig");` / `pub const PRNG = @import("prng.zig");` in a
|
||||
// file that declares nothing itself. Consumers then write
|
||||
// `terminal.Terminal.init()`, `stdx.PRNG.from_seed()`, `t: stdx.Thing`.
|
||||
// Measured on real projects before → after this flag: CALLS into ghostty's
|
||||
// `src/terminal/` from outside it 46 → 253; into tigerbeetle's `stdx` hub
|
||||
// from outside it 837 → 1500 (136 `stdx.Type.fn(` sites, 289 annotations).
|
||||
namespaceExportsIncludeImportedNames: true,
|
||||
// A qualified receiver is a chain of `const` handles — hub modules
|
||||
// republishing modules (`hub.sub.Thing{}`), types nested in types
|
||||
// (`mod.Outer.Inner{}`), enum variants through the module
|
||||
// (`opmod.Op.lookup.event_max()`) — walked hop by hop from the verified
|
||||
// import; a one-hop split at the last dot resolved none of them.
|
||||
resolveNamespaceChains: true,
|
||||
|
||||
loadResolutionConfig: (repoPath: string) => loadZigBuildConfig(repoPath),
|
||||
|
||||
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) =>
|
||||
resolveZigImportInternal(
|
||||
fromFile,
|
||||
targetRaw,
|
||||
allFilePaths,
|
||||
(resolutionConfig as ZigBuildZonConfig | null | undefined) ?? null,
|
||||
),
|
||||
|
||||
// `pub usingnamespace @import("x.zig");` — target decls become local decls.
|
||||
expandsWildcardTo: (targetModuleScope, parsedFiles) =>
|
||||
expandZigWildcardNames(targetModuleScope, parsedFiles),
|
||||
|
||||
mergeBindings: zigMergeBindings,
|
||||
arityCompatibility: zigArityCompatibility,
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
// Payload captures — `for (items) |it|`, `if (opt) |v|`, `while (it.next())
|
||||
// |x|` — typed from the subject's binding after finalize (F6).
|
||||
populateRangeBindings: populateZigRangeBindings,
|
||||
|
||||
// Zig has no `super`.
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
// Statically typed — the field-fallback heuristic over-connects.
|
||||
fieldFallbackOnMethodLookup: false,
|
||||
};
|
||||
164
gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts
Normal file
164
gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import type {
|
||||
BindingRef,
|
||||
Callsite,
|
||||
CaptureMatch,
|
||||
ParsedFile,
|
||||
Scope,
|
||||
ScopeId,
|
||||
ScopeTree,
|
||||
SymbolDefinition,
|
||||
TypeRef,
|
||||
} from 'gitnexus-shared';
|
||||
|
||||
/** Keep parameter (incl. `self`) typeBindings in the function scope —
|
||||
* hoisting them to Module would pollute other functions' receiver
|
||||
* resolution (same rationale as `goBindingScopeFor`). */
|
||||
export function zigBindingScopeFor(
|
||||
decl: CaptureMatch,
|
||||
innermost: Scope,
|
||||
tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
if (decl['@type-binding.parameter'] !== undefined) {
|
||||
return innermost.id;
|
||||
}
|
||||
// A container returned by a generic type constructor (`fn List(comptime T:
|
||||
// type) type { return struct {…}; }`) is anchored on the container node,
|
||||
// whose innermost enclosing scope is the FUNCTION BODY. Its name is only
|
||||
// useful to callers (`List(u8){}`, `List(u8).init()`), so bind it in the
|
||||
// module scope beside the Function def of the same name — `List` really is
|
||||
// both a callable and a type. `zigTypeConstructorOf` in the walker decides
|
||||
// WHICH containers get this rule; the marker capture carries the verdict.
|
||||
if (decl['@declaration.type-constructor'] !== undefined) {
|
||||
let scope: Scope | undefined = innermost;
|
||||
while (scope !== undefined && scope.kind !== 'Module') scope = tree.getParent(scope.id);
|
||||
return scope?.id ?? null;
|
||||
}
|
||||
// File-struct members: the file's Class scope spans the whole file (same
|
||||
// range as the Module scope, nested under it). Their DEFS stay owned by the
|
||||
// Class scope (that is what makes them methods/fields of the file's Struct),
|
||||
// but their NAMES must stay visible at module level — `Page.init()` from
|
||||
// another file is a namespace-member lookup over the module scope's
|
||||
// bindings, and `usingnamespace` expansion reads the same. Hoist every
|
||||
// declaration binding hosted by an equal-range Class scope to its Module
|
||||
// parent. Type bindings (`@type-binding.*`, incl. field types) are NOT
|
||||
// hoisted: the compound resolver reads member types from the class scope.
|
||||
const anchor = ZIG_DECLARATION_ANCHORS.map((k) => decl[k]).find((c) => c !== undefined);
|
||||
if (anchor !== undefined) {
|
||||
// Reproduce the extractor's auto-hoist (a scope-creating declaration is
|
||||
// bound in the scope OUTSIDE its own body), then step past the file's
|
||||
// Class scope when that is where the name would land.
|
||||
let host: Scope | undefined = innermost;
|
||||
if (host.parent !== null && sameRange(anchor.range, host.range)) {
|
||||
host = tree.getParent(host.id);
|
||||
}
|
||||
if (host !== undefined && host.kind === 'Class' && host.parent !== null) {
|
||||
const parent = tree.getParent(host.id);
|
||||
if (parent !== undefined && parent.kind === 'Module' && sameRange(parent.range, host.range)) {
|
||||
return parent.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null; // default auto-hoist for other bindings
|
||||
}
|
||||
|
||||
const ZIG_DECLARATION_ANCHORS = [
|
||||
'@declaration.function',
|
||||
'@declaration.method',
|
||||
'@declaration.struct',
|
||||
'@declaration.enum',
|
||||
'@declaration.union',
|
||||
'@declaration.field',
|
||||
'@declaration.variable',
|
||||
] as const;
|
||||
|
||||
function sameRange(a: Scope['range'], b: Scope['range']): boolean {
|
||||
return (
|
||||
a.startLine === b.startLine &&
|
||||
a.startCol === b.startCol &&
|
||||
a.endLine === b.endLine &&
|
||||
a.endCol === b.endCol
|
||||
);
|
||||
}
|
||||
|
||||
/** `pub usingnamespace @import("x.zig");` — every declaration of the target
|
||||
* module becomes a declaration of the importer. Enumerates the target's
|
||||
* module-level names for the finalize wildcard expansion (Dart pattern).
|
||||
* Zig visibility (`pub`) is not recorded on scope-side defs, so this
|
||||
* over-approximates to every top-level name; the structure phase's
|
||||
* `isExported` is the authoritative visibility. */
|
||||
export function expandZigWildcardNames(
|
||||
targetModuleScope: ScopeId,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): readonly string[] {
|
||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
||||
if (target === undefined) return [];
|
||||
const seen = new Set<string>();
|
||||
const names: string[] = [];
|
||||
for (const def of target.localDefs) {
|
||||
const qn = def.qualifiedName;
|
||||
if (qn === undefined || qn.length === 0 || qn.includes('.')) continue; // top-level only
|
||||
if (seen.has(qn)) continue;
|
||||
seen.add(qn);
|
||||
names.push(qn);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Zig's receiver convention is a FIRST parameter named `self`; the
|
||||
* `self`-sourced typeBinding on the function scope carries its type.
|
||||
* Position is enforced upstream, not here: `interpretZigTypeBinding` only
|
||||
* sources a binding as `self` when `emitZigScopeCaptures` tagged it
|
||||
* `@type-binding.first-parameter`, so a later parameter named `self`
|
||||
* arrives as `parameter-annotation` and is never returned by this hook. */
|
||||
export function zigReceiverBinding(functionScope: Scope): TypeRef | null {
|
||||
if (functionScope.kind !== 'Function') return null;
|
||||
for (const binding of functionScope.typeBindings.values()) {
|
||||
if (binding.source === 'self') return binding;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const TIER: Record<BindingRef['origin'], number> = {
|
||||
local: 0,
|
||||
namespace: 1,
|
||||
import: 2,
|
||||
reexport: 3,
|
||||
wildcard: 4,
|
||||
};
|
||||
|
||||
/** Local declarations shadow imports; deterministic order within a tier. */
|
||||
export function zigMergeBindings(
|
||||
existing: readonly BindingRef[],
|
||||
incoming: readonly BindingRef[],
|
||||
_scopeId: string,
|
||||
): BindingRef[] {
|
||||
const all = [...existing, ...incoming];
|
||||
if (all.length === 0) return [];
|
||||
let bestTier = 99;
|
||||
for (const b of all) {
|
||||
const t = TIER[b.origin] ?? 99;
|
||||
if (t < bestTier) bestTier = t;
|
||||
}
|
||||
const byNode = new Map<string, BindingRef>();
|
||||
for (const b of all) {
|
||||
if ((TIER[b.origin] ?? 99) !== bestTier) continue;
|
||||
if (!byNode.has(b.def.nodeId)) byNode.set(b.def.nodeId, b);
|
||||
}
|
||||
return [...byNode.values()].sort((a, b) => a.def.nodeId.localeCompare(b.def.nodeId));
|
||||
}
|
||||
|
||||
/** Zig has no overloading; without synthesized arity metadata on
|
||||
* declarations the comparison is always 'unknown' — kept as a real
|
||||
* bounds check so it turns on if arity captures are added later. */
|
||||
export function zigArityCompatibility(
|
||||
callsite: Callsite,
|
||||
def: SymbolDefinition,
|
||||
): 'compatible' | 'unknown' | 'incompatible' {
|
||||
const max = def.parameterCount;
|
||||
const min = def.requiredParameterCount;
|
||||
if (max === undefined && min === undefined) return 'unknown';
|
||||
if (!Number.isFinite(callsite.arity) || callsite.arity < 0) return 'unknown';
|
||||
if (min !== undefined && callsite.arity < min) return 'incompatible';
|
||||
if (max !== undefined && callsite.arity > max) return 'incompatible';
|
||||
return 'compatible';
|
||||
}
|
||||
141
gitnexus/src/core/ingestion/method-extractors/configs/zig.ts
Normal file
141
gitnexus/src/core/ingestion/method-extractors/configs/zig.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { MethodExtractionConfig, ParameterInfo } from '../../method-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { hasZigPubKeyword } from '../../export-detection.js';
|
||||
import {
|
||||
ZIG_CONTAINER_TYPES,
|
||||
zigContainerName,
|
||||
zigReceiverParameter,
|
||||
} from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig method extraction.
|
||||
*
|
||||
* tree-sitter-zig containers (struct/enum/union/opaque) are anonymous; the
|
||||
* binding name lives on the parent variable_declaration, or on the enclosing
|
||||
* generic type constructor (`fn List(comptime T: type) type { return struct
|
||||
* {…}; }`) — `zigContainerName` decides. Methods inside a container appear as
|
||||
* plain `function_declaration` children of the container node.
|
||||
*
|
||||
* The first parameter is the receiver when it is named `self` OR typed as the
|
||||
* enclosing container (`replica: *Replica`, `pool: *@This()`) — see
|
||||
* `zigReceiverParameter`; unlike Rust, Zig has no dedicated `self_parameter`
|
||||
* node type and `self` is a convention, not a rule.
|
||||
*/
|
||||
|
||||
const extractZigOwnerName = (node: SyntaxNode, filePath?: string): string | undefined =>
|
||||
zigContainerName(node, filePath);
|
||||
|
||||
const extractZigName = (node: SyntaxNode): string | undefined => {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
return nameNode?.text;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `parameters` node of a function_declaration. tree-sitter-zig 1.1.2
|
||||
* attaches it as a plain named child — NOT under a `parameters:` field (only
|
||||
* `name`, `type` and `body` are fields), so a field lookup is always null
|
||||
* (and the grammar-literal gate flags it as a dead field). Reading it that
|
||||
* way silently produced empty parameter lists, no receiver, and
|
||||
* `isStatic: true` for every method.
|
||||
*/
|
||||
const zigParameterList = (node: SyntaxNode): SyntaxNode | null =>
|
||||
node.namedChildren.find((child): child is SyntaxNode => child?.type === 'parameters') ?? null;
|
||||
|
||||
const extractZigReturnType = (node: SyntaxNode): string | undefined => {
|
||||
// tree-sitter-zig labels the return type as the `type` field on
|
||||
// function_declaration (the same field name used for parameter types).
|
||||
const typeNode = node.childForFieldName('type');
|
||||
return typeNode?.text?.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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`). `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, filePath?: string): ParameterInfo[] => {
|
||||
const paramList = zigParameterList(node);
|
||||
if (!paramList) return [];
|
||||
const params: ParameterInfo[] = [];
|
||||
const receiver = zigReceiverParameter(node, filePath);
|
||||
for (let i = 0; i < paramList.namedChildCount; i++) {
|
||||
const param = paramList.namedChild(i);
|
||||
if (!param || param.type !== 'parameter') continue;
|
||||
if (receiver !== null && param.id === receiver.id) continue;
|
||||
const nameNode = param.childForFieldName('name');
|
||||
const typeNode = param.childForFieldName('type');
|
||||
params.push({
|
||||
name: nameNode?.text ?? '?',
|
||||
type: typeNode?.text?.trim() ?? null,
|
||||
rawType: typeNode?.text?.trim() ?? null,
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
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
|
||||
* `findEnclosingFunctionId`) with the SAME spelling ZIG_QUERIES captures as
|
||||
* `@name` — the string node with its quotes — so calls inside `test "x" {}`
|
||||
* attribute to the test's own Function node.
|
||||
*
|
||||
* Anonymous `test {}` and decl-tests `test add {}` are not graph nodes. They
|
||||
* return `''`, not `null`: `null` falls through to `genericFuncName`, whose
|
||||
* first-identifier scan would name `test add {}` "add" — the REAL `fn add`'s
|
||||
* id — and hang the test body's calls on it. The empty name is falsy, so
|
||||
* `findEnclosingFunctionId` skips this node WITHOUT attributing to it and
|
||||
* keeps walking up; a test block can only sit at container level, so the walk
|
||||
* reaches the file and the calls attribute to the File.
|
||||
*/
|
||||
const extractZigFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: 'Function' } | null => {
|
||||
if (node.type !== 'test_declaration') return null;
|
||||
const nameString = node.namedChildren.find(
|
||||
(child): child is SyntaxNode => child?.type === 'string',
|
||||
);
|
||||
return { funcName: nameString?.text ?? '', label: 'Function' };
|
||||
};
|
||||
|
||||
export const zigMethodConfig: MethodExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
// `source_file`: a file-struct's top-level fns are methods of the file's Struct.
|
||||
typeDeclarationNodes: [...ZIG_CONTAINER_TYPES, 'source_file'],
|
||||
methodNodeTypes: ['function_declaration'],
|
||||
bodyNodeTypes: [],
|
||||
extractOwnerName: extractZigOwnerName,
|
||||
extractName: extractZigName,
|
||||
extractFunctionName: extractZigFunctionName,
|
||||
extractReturnType: extractZigReturnType,
|
||||
extractParameters: extractZigParameters,
|
||||
// `pub` only: `export fn` is C linkage, still private to other Zig files
|
||||
// (isExported carries the FFI fact — see export-detection.ts).
|
||||
extractVisibility: (node) => (hasZigPubKeyword(node) ? 'public' : 'private'),
|
||||
extractReceiverType: extractZigReceiverType,
|
||||
|
||||
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()`, `ledger: *Ledger` in `Ledger.zig`); see
|
||||
// `zigReceiverParameter`.
|
||||
return zigReceiverParameter(node, filePath) === null;
|
||||
},
|
||||
|
||||
isAbstract() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isFinal() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
|
@ -90,7 +90,7 @@ export function createMethodExtractor(config: MethodExtractionConfig): MethodExt
|
|||
// Resolve owner name: config hook → field-based → type_identifier → simple_identifier → "Companion"
|
||||
let ownerName: string | undefined;
|
||||
if (config.extractOwnerName) {
|
||||
ownerName = config.extractOwnerName(node);
|
||||
ownerName = config.extractOwnerName(node, context.filePath);
|
||||
}
|
||||
if (!ownerName) {
|
||||
const nameField = node.childForFieldName('name');
|
||||
|
|
@ -166,6 +166,17 @@ function findBodies(node: SyntaxNode, bodyNodeSet: Set<string>): SyntaxNode[] {
|
|||
result.push(bodyField);
|
||||
addNestedBodies(bodyField, bodyNodeSet, result);
|
||||
}
|
||||
// Grammars with no body wrapper at all: a config that declares NO
|
||||
// `bodyNodeTypes` (tree-sitter-zig's struct_declaration holds its
|
||||
// function_declaration children directly) uses the type-declaration node
|
||||
// itself as the body. The downstream walk filters by `methodNodeTypes`, so
|
||||
// unrelated children are ignored. Deliberately NOT a fallback for configs
|
||||
// that do declare body wrappers: for them a node without its wrapper is a
|
||||
// bodiless declaration (forward declaration, `declare class`), and scanning
|
||||
// it would change every such language for no method it could find.
|
||||
if (result.length === 0 && bodyNodeSet.size === 0) {
|
||||
result.push(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -236,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;
|
||||
|
|
@ -107,7 +113,7 @@ export interface MethodExtractionConfig {
|
|||
* When the ownerNode matches one of these types, isStatic is forced true. */
|
||||
staticOwnerTypes?: ReadonlySet<string>;
|
||||
/** Resolve the owner name from a standalone method node (e.g. Go receiver type). */
|
||||
extractOwnerName?: (node: SyntaxNode) => string | undefined;
|
||||
extractOwnerName?: (node: SyntaxNode, filePath?: string) => string | undefined;
|
||||
/** Extract a primary constructor from the owner node itself (e.g. C# 12 class Point(int x, int y)). */
|
||||
extractPrimaryConstructor?: (
|
||||
ownerNode: SyntaxNode,
|
||||
|
|
|
|||
|
|
@ -322,6 +322,10 @@ function buildReference(site: ReferenceSite, top: Resolution): Reference {
|
|||
toDef: top.def.nodeId,
|
||||
atRange: site.atRange,
|
||||
kind: site.kind,
|
||||
// The call form survives resolution so the graph bridge can mark
|
||||
// construction sites (`callForm: 'constructor'`) on the CALLS edge it
|
||||
// emits — a `Reference` otherwise keeps only the resolved def.
|
||||
...(site.kind === 'call' && site.callForm !== undefined ? { callForm: site.callForm } : {}),
|
||||
confidence: top.confidence,
|
||||
evidence: top.evidence,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -878,6 +878,71 @@ export interface ScopeResolver {
|
|||
*/
|
||||
readonly constructorCallTargetsClass?: boolean;
|
||||
|
||||
/**
|
||||
* When true, the CALLS edge emitted for a constructor-form site
|
||||
* (`callForm === 'constructor'`) carries ` (constructor)` appended to its
|
||||
* `reason` — `local-call (constructor)`, `import-resolved (constructor)`,
|
||||
* `scope-resolution: call (constructor)` — so a consumer can tell
|
||||
* "constructs an instance of" apart from "invokes" on the edge alone.
|
||||
*
|
||||
* Opt-in because the unsuffixed strings are a pinned contract: the legacy
|
||||
* DAG vocabulary (`'import-resolved' | 'local-call' | …`, see the
|
||||
* same-graph guarantee above) is asserted verbatim by consumers and by the
|
||||
* per-language resolver suites, constructor sites included. A language
|
||||
* that links a construction site to the TYPE node itself — a struct
|
||||
* literal `T{…}` in Zig, where nothing but the marker distinguishes the
|
||||
* edge from an invocation in the schema (PR #1432 review) — opts in; the
|
||||
* default leaves every existing edge byte-identical.
|
||||
*
|
||||
* The marker rides in `reason` because relationships carry no arbitrary
|
||||
* properties (adding one moves SCHEMA_FINGERPRINT — the IMPLEMENTS
|
||||
* `-pointer` precedent in `pipeline/run.ts`). Applies to the free-call
|
||||
* fallback, the reference bridge and the receiver-bound paths — a
|
||||
* namespace-qualified literal (`mod.T{…}`, Case 1), a type nested in the
|
||||
* receiver's class (`A.Item{}`, Case 2) and a dotted type binding (Case 3)
|
||||
* all go through `constructionSiteReason` — so an opted-in provider sees
|
||||
* one vocabulary whichever path resolved the site.
|
||||
*/
|
||||
readonly markConstructionSites?: boolean;
|
||||
|
||||
/**
|
||||
* When true, a namespace's exported member may also be a name the target
|
||||
* module IMPORTED and publishes as its own — the hub-module shape, a file
|
||||
* made only of re-exports (`pub const Terminal = @import("Terminal.zig");`,
|
||||
* `pub const Thing = @import("thing.zig").Thing;`). Such a file owns no
|
||||
* local binding, so the default local-only export lookup
|
||||
* (`findExportedDef`) finds nothing for `terminal.Terminal.init()`,
|
||||
* `t: stdx.Thing`, `var p = stdx.PRNG.from_seed()` or `var a:
|
||||
* stdx.BoundedArrayType(u8, 4)`, and the receiver-bound namespace paths
|
||||
* (Case 1, Case 3, the compound resolver's namespace branch) fall through.
|
||||
* With the flag those paths use `findExportedDefIncludingImportedNames`,
|
||||
* which reads the finalized channel where the published names live.
|
||||
*
|
||||
* Off by default: in most languages a module's imports are not its exports
|
||||
* (TypeScript `import { X }` publishes nothing), and the finalized edge does
|
||||
* not record whether the import was written `pub`. Zig opts in — a hub
|
||||
* member a consumer can name through the hub is public by construction.
|
||||
*/
|
||||
readonly namespaceExportsIncludeImportedNames?: boolean;
|
||||
|
||||
/**
|
||||
* When true, a qualified receiver is walked SEGMENT BY SEGMENT from its
|
||||
* verified namespace root instead of being split once at the last dot:
|
||||
* `hub.sub.Thing{}` (a namespace republished by a hub — `pub const sub =
|
||||
* @import("sub.zig");`), `mod.Outer.Inner{}` (a type nested in a type),
|
||||
* `opmod.Op.lookup` (an enum variant reached through the module), and the
|
||||
* typed forms `x: mod.Outer.Inner`. Each hop is either a class-like member
|
||||
* of the current module(s) / the current class, or a namespace import
|
||||
* edge the current module's scope binds under that name; a hop that is
|
||||
* ambiguous — two files behind one handle disagree, or a name is both a
|
||||
* type and a republished module — resolves nothing rather than picking a
|
||||
* first match. Off, the receiver-bound paths (Case 1, Case 2's
|
||||
* namespace-qualified class, Case 3) keep their one-hop lookups exactly
|
||||
* as they are, so no existing edge moves; Zig opts in (PR #1432 review,
|
||||
* 8.10), whose module system is nothing but nested `const` handles.
|
||||
*/
|
||||
readonly resolveNamespaceChains?: boolean;
|
||||
|
||||
/**
|
||||
* How this language spells a construction expression, so the compound
|
||||
* receiver resolver can type an INLINE constructor receiver — the
|
||||
|
|
|
|||
|
|
@ -288,6 +288,15 @@ export const LINKABLE_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
|
|||
// are unreachable and label-agnostic fallback can alias it to a same-named
|
||||
// Constructor or Method (#2801).
|
||||
'Record',
|
||||
// Union is linkable because Zig wires `union` / `union(enum)` as a member
|
||||
// container: the definition phase emits HAS_METHOD / HAS_PROPERTY edges FROM
|
||||
// the Union node (`union_declaration` in MEMBER_OWNER_NODE_TYPES) and the
|
||||
// scope side dispatches methods on union receivers (`main → isEnergy` in
|
||||
// test/integration/resolvers/zig.test.ts). Without this entry the schema
|
||||
// never declares a `FROM Union` pair and `analyze` aborts on the first Zig
|
||||
// repo that declares a union (reproduced on the zig-basic fixture itself).
|
||||
// Also lets `Tag{ .energy = 5 }` constructor references bridge to the node.
|
||||
'Union',
|
||||
// Trait nodes are linkable so MRO builders can bridge PHP/Rust trait
|
||||
// defs between scope-resolution DefIds and the graph's node ids.
|
||||
// IMPLEMENTS edges from classes to traits are otherwise invisible to
|
||||
|
|
@ -303,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, Record, Union, 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',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* chain looking for an enclosing Function/Method/Class.
|
||||
* 2. Resolve `toDef` → target graph-node id via `nodeLookup`.
|
||||
* 3. Emit the edge (`CALLS` / `READS` / `WRITES` / `EXTENDS` / `USES`)
|
||||
* with the standard reason format.
|
||||
* with the standard reason format (`referenceEdgeReason`).
|
||||
*
|
||||
* Skips (without throwing) when either side fails to map — either side
|
||||
* may legitimately not exist as a graph node (e.g. a resolved target
|
||||
|
|
@ -36,6 +36,36 @@ import { isValueDefinitionLabel } from '../../utils/ast-helpers.js';
|
|||
*/
|
||||
type ReferenceSiteSkipSet = ReadonlySet<string>;
|
||||
|
||||
export interface EmitReferencesOptions {
|
||||
/** When true, a constructor-form call site's edge gets ` (constructor)`
|
||||
* appended to its reason. See `ScopeResolver.markConstructionSites`. */
|
||||
readonly markConstructionSites?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* `reason` of the edge a resolved reference emits: `scope-resolution: <kind>`,
|
||||
* plus ` (constructor)` for a construction site when the provider opted in —
|
||||
* a struct literal (`T{…}` in Zig, `T { .. }` in Rust, `T{}` in Go) or a
|
||||
* `new T()` resolves to the type (or its constructor) as a CALLS edge exactly
|
||||
* like an invocation does, and nothing else on the edge tells the two apart
|
||||
* (PR #1432 review).
|
||||
*
|
||||
* The marker rides in `reason` because relationships carry no arbitrary
|
||||
* properties — adding one would change the relation DDL and move
|
||||
* SCHEMA_FINGERPRINT (see the IMPLEMENTS `-pointer` precedent in
|
||||
* `pipeline/run.ts`). The plain `scope-resolution: call` prefix is kept, so a
|
||||
* consumer matching the prefix still sees every call; one matching the exact
|
||||
* string sees invocations only.
|
||||
*/
|
||||
export function referenceEdgeReason(
|
||||
ref: Pick<Reference, 'kind' | 'callForm'>,
|
||||
markConstructionSites: boolean | undefined,
|
||||
): string {
|
||||
return markConstructionSites === true && ref.kind === 'call' && ref.callForm === 'constructor'
|
||||
? 'scope-resolution: call (constructor)'
|
||||
: `scope-resolution: ${ref.kind}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value labels whose defs MAY be function-local. A reference to one of these is
|
||||
* dropped only when the def is positively identified as living inside a function
|
||||
|
|
@ -76,6 +106,7 @@ export function emitReferencesViaLookup(
|
|||
* Optional so callers that never capture bare identifiers are unchanged.
|
||||
*/
|
||||
functionLocalValueDefIds?: ReadonlySet<string>,
|
||||
options?: EmitReferencesOptions,
|
||||
): { emitted: number; skipped: number } {
|
||||
let emitted = 0;
|
||||
let skipped = 0;
|
||||
|
|
@ -153,7 +184,7 @@ export function emitReferencesViaLookup(
|
|||
targetId: targetGraphId,
|
||||
type: edgeType,
|
||||
confidence: ref.confidence,
|
||||
reason: `scope-resolution: ${ref.kind}`,
|
||||
reason: referenceEdgeReason(ref, options?.markConstructionSites),
|
||||
});
|
||||
emitted++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
findEnclosingClassDef,
|
||||
findExportedDef,
|
||||
findExportedDefByName,
|
||||
findExportedDefIncludingImportedNames,
|
||||
findReceiverTypeBinding,
|
||||
isClassLike,
|
||||
isNamespaceNameShadowed,
|
||||
|
|
@ -128,6 +129,20 @@ interface ResolveCompoundReceiverOptions {
|
|||
readonly constructionSyntax?: ScopeResolver['constructionSyntax'];
|
||||
/** Verified namespace handles visible in the current file. */
|
||||
readonly namespaceTargets?: ReadonlyMap<string, readonly string[]>;
|
||||
/** A namespace member may be a name the target module imported and
|
||||
* publishes (hub modules). See `ScopeResolver.namespaceExportsIncludeImportedNames`. */
|
||||
readonly namespaceExportsIncludeImportedNames?: boolean;
|
||||
/** Resolve a qualified CLASS name (`opmod.Op`, `hub.sub.Thing`,
|
||||
* `mod.Outer.Inner`) through the language's namespace chain walk
|
||||
* (`ScopeResolver.resolveNamespaceChains`). Seeds the dotted-chain walk
|
||||
* when its head is a namespace rather than a value: `opmod.Op.lookup` is
|
||||
* the enum `Op` reached through the module `opmod`, then its variant
|
||||
* `lookup` — a value of `Op` — and only then a method (PR #1432 review,
|
||||
* 8.10). Absent ⇒ the head must bind in scope, exactly as before. */
|
||||
readonly resolveQualifiedClass?: (
|
||||
qualifiedName: string,
|
||||
inScope: ScopeId,
|
||||
) => SymbolDefinition | undefined;
|
||||
/** Compact receiver chain for THIS site (`ReferenceSite.receiverChain`), when
|
||||
* the language's capture emitter produced one. Present ⇒ the structural fold
|
||||
* is tried before the text cascade; absent ⇒ behaviour is exactly as before.
|
||||
|
|
@ -241,7 +256,11 @@ function resolveConstructionExpressionClass(
|
|||
if (namespaceFiles.length > 0) {
|
||||
if (isNamespaceNameShadowed(namespaceName, inScope, scopes)) return undefined;
|
||||
const namespaceMatches = namespaceFiles
|
||||
.map((targetFile) => findExportedDef(targetFile, exportedName, index))
|
||||
.map((targetFile) =>
|
||||
options.namespaceExportsIncludeImportedNames === true
|
||||
? findExportedDefIncludingImportedNames(targetFile, exportedName, index, scopes)
|
||||
: findExportedDef(targetFile, exportedName, index),
|
||||
)
|
||||
.filter((def): def is SymbolDefinition => def !== undefined && isClassLike(def.type));
|
||||
return namespaceMatches.length === 1 ? namespaceMatches[0] : undefined;
|
||||
}
|
||||
|
|
@ -1179,8 +1198,30 @@ export function resolveCompoundReceiverClass(
|
|||
options,
|
||||
);
|
||||
}
|
||||
// Namespace-qualified chain head — `opmod.Op.lookup` / `hub.sub.Thing.x`:
|
||||
// no binding and no class named `opmod`, but the LONGEST prefix the
|
||||
// language's chain walk accepts as a class seeds the walk (the class
|
||||
// itself, so a variant / static member hop is read off the class scope),
|
||||
// and the remaining segments are walked as members. Longest first: the
|
||||
// prefix is a class, not a value, and `a.B.C` must seed at `C`, not stop
|
||||
// at `B` and read `C` as a member of it. The whole receiver may be the
|
||||
// class (`opmod.Op` — no member segment left), exactly as a bare class
|
||||
// name head resolves to the class constant above.
|
||||
let firstHop = 1;
|
||||
if (currentClass === undefined && headType === undefined && options.resolveQualifiedClass) {
|
||||
for (let k = parts.length; k >= 2; k--) {
|
||||
const prefix = parts.slice(0, k).join('.');
|
||||
if (prefix.includes('(')) continue;
|
||||
const seeded = options.resolveQualifiedClass(prefix, inScope);
|
||||
if (seeded === undefined) continue;
|
||||
currentClass = seeded;
|
||||
currentIsClassConstant = true;
|
||||
firstHop = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 1; i < parts.length && currentClass !== undefined; i++) {
|
||||
for (let i = firstHop; i < parts.length && currentClass !== undefined; i++) {
|
||||
const segment = parts[i];
|
||||
if (segment === undefined) break;
|
||||
const memberName = stripCallParens(segment);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type {
|
|||
ParameterTypeClass,
|
||||
ParsedFile,
|
||||
Reference,
|
||||
ReferenceSite,
|
||||
ScopeId,
|
||||
SymbolDefinition,
|
||||
} from 'gitnexus-shared';
|
||||
|
|
@ -66,6 +67,9 @@ export function emitFreeCallFallback(
|
|||
/** When true, `Type(...)` constructor calls link to the Class def
|
||||
* itself rather than its explicit Constructor. Swift opts in. */
|
||||
readonly constructorCallTargetsClass?: boolean;
|
||||
/** When true, a constructor-form site's edge gets ` (constructor)`
|
||||
* appended to its reason. See `ScopeResolver.markConstructionSites`. */
|
||||
readonly markConstructionSites?: boolean;
|
||||
readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
|
||||
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||
readonly callerParsed: ParsedFile;
|
||||
|
|
@ -621,8 +625,13 @@ export function emitFreeCallFallback(
|
|||
type: 'CALLS',
|
||||
confidence: 0.85,
|
||||
// Match legacy DAG's reason convention so consumers that
|
||||
// assert `reason === 'import-resolved'` keep working.
|
||||
reason: fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call',
|
||||
// assert `reason === 'import-resolved'` keep working. The
|
||||
// construction-site marker is opt-in for the same reason.
|
||||
reason: constructionSiteReason(
|
||||
fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call',
|
||||
site,
|
||||
options.markConstructionSites,
|
||||
),
|
||||
});
|
||||
emitted++;
|
||||
}
|
||||
|
|
@ -630,6 +639,19 @@ export function emitFreeCallFallback(
|
|||
return emitted;
|
||||
}
|
||||
|
||||
/** `reason` of a free-call edge: the legacy string, plus ` (constructor)` for
|
||||
* a construction site when the provider opted in
|
||||
* (`ScopeResolver.markConstructionSites`). */
|
||||
export function constructionSiteReason(
|
||||
base: string,
|
||||
site: Pick<ReferenceSite, 'callForm'>,
|
||||
markConstructionSites: boolean | undefined,
|
||||
): string {
|
||||
return markConstructionSites === true && site.callForm === 'constructor'
|
||||
? `${base} (constructor)`
|
||||
: base;
|
||||
}
|
||||
|
||||
function siteKey(
|
||||
filePath: string,
|
||||
site: { readonly atRange: { readonly startLine: number; readonly startCol: number } },
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
* resolved to a wrong target.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { SemanticModel } from '../../model/semantic-model.js';
|
||||
|
|
@ -73,6 +73,7 @@ import {
|
|||
findEnclosingClassDef,
|
||||
isReceiverOwnedButUnbound,
|
||||
findExportedDef,
|
||||
findExportedDefIncludingImportedNames,
|
||||
findOwnedMember,
|
||||
findReceiverTypeBinding,
|
||||
findValueBindingInScope,
|
||||
|
|
@ -86,6 +87,7 @@ import {
|
|||
tryEmitEdgeWithExplicitTargetId,
|
||||
type CalleeIdCaptureCtx,
|
||||
} from '../graph-bridge/edges.js';
|
||||
import { constructionSiteReason } from './free-call-fallback.js';
|
||||
import type { CalleeIdSink } from '../graph-bridge/callee-id-sink.js';
|
||||
import {
|
||||
resolveCompoundReceiverClass,
|
||||
|
|
@ -115,6 +117,53 @@ import type { DecodedReceiverChain } from '../../utils/receiver-chain-codec.js';
|
|||
/** Subset of `ScopeResolver` consumed by this pass. Accepting the
|
||||
* subset rather than the full provider keeps tests and partial
|
||||
* refactors lighter — callers only need to populate what we read. */
|
||||
/** Split `text` at the dots that sit at nesting depth 0 and outside string
|
||||
* literals — `@import("a.zig").Outer.Inner` → three segments, not four;
|
||||
* `List(u8).Node` → two. The chain walk's segmenter. */
|
||||
function splitTopLevelDots(text: string): string[] {
|
||||
const out: string[] = [];
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let start = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i]!;
|
||||
if (inString) {
|
||||
if (ch === '\\') i++;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') inString = true;
|
||||
else if (ch === '(' || ch === '[' || ch === '<') depth++;
|
||||
else if (ch === ')' || ch === ']' || ch === '>') depth--;
|
||||
else if (ch === '.' && depth === 0) {
|
||||
out.push(text.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
out.push(text.slice(start));
|
||||
return out.filter((s) => s.length > 0);
|
||||
}
|
||||
|
||||
/** Index of the last depth-0, outside-string dot of `text`, or -1. */
|
||||
function lastTopLevelDot(text: string): number {
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let last = -1;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i]!;
|
||||
if (inString) {
|
||||
if (ch === '\\') i++;
|
||||
else if (ch === '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') inString = true;
|
||||
else if (ch === '(' || ch === '[' || ch === '<') depth++;
|
||||
else if (ch === ')' || ch === ']' || ch === '>') depth--;
|
||||
else if (ch === '.' && depth === 0) last = i;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
type ReceiverBoundProviderSubset = Pick<
|
||||
ScopeResolver,
|
||||
| 'isSuperReceiver'
|
||||
|
|
@ -135,6 +184,9 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'constraintCompatibility'
|
||||
| 'isStaticOnly'
|
||||
| 'normalizeTypeArgument'
|
||||
| 'markConstructionSites'
|
||||
| 'namespaceExportsIncludeImportedNames'
|
||||
| 'resolveNamespaceChains'
|
||||
>;
|
||||
|
||||
/** A bare, undecorated identifier and nothing else — see {@link isBareTypeName}. */
|
||||
|
|
@ -329,9 +381,145 @@ export function emitReceiverBoundCalls(
|
|||
const fieldFallback = provider.fieldFallbackOnMethodLookup ?? true;
|
||||
const collapse = provider.collapseMemberCallsByCallerTarget === true;
|
||||
const hoistTypeBindingsToModule = provider.hoistTypeBindingsToModule === true;
|
||||
// Namespace-member lookup for Case 1 / Case 3: local exports only, unless
|
||||
// the provider publishes imported names too (hub modules — see
|
||||
// `ScopeResolver.namespaceExportsIncludeImportedNames`).
|
||||
const lookupNamespaceMember = (targetFile: string, name: string): SymbolDefinition | undefined =>
|
||||
provider.namespaceExportsIncludeImportedNames === true
|
||||
? findExportedDefIncludingImportedNames(targetFile, name, index, scopes)
|
||||
: findExportedDef(targetFile, name, index);
|
||||
// A class-like member `name` unique across `files`, or nothing — two
|
||||
// same-named classes behind one handle would mint a confident wrong edge.
|
||||
const uniqueClassAcross = (
|
||||
files: readonly string[],
|
||||
name: string,
|
||||
): SymbolDefinition | undefined => {
|
||||
let picked: SymbolDefinition | undefined;
|
||||
for (const file of files) {
|
||||
const def = lookupNamespaceMember(file, name);
|
||||
if (def === undefined || !isClassLike(def.type)) continue;
|
||||
if (picked !== undefined && picked.nodeId !== def.nodeId) return undefined;
|
||||
picked = def;
|
||||
}
|
||||
return picked;
|
||||
};
|
||||
// A class-like def NESTED in `owner` (`A.Item` inside `A`): its qualified
|
||||
// name is the owner's plus the segment — the identity the structure phase
|
||||
// and `populateClassOwnedMembers` agree on — so the qualified-name index
|
||||
// answers directly; same file as the owner, unique or nothing. Only the
|
||||
// chain walk reads this: `findOwnedMember` knows methods and fields, and a
|
||||
// nested type is neither.
|
||||
const findNestedClass = (owner: SymbolDefinition, name: string): SymbolDefinition | undefined => {
|
||||
if (owner.qualifiedName === undefined || owner.qualifiedName.length === 0) return undefined;
|
||||
let picked: SymbolDefinition | undefined;
|
||||
for (const id of scopes.qualifiedNames.get(`${owner.qualifiedName}.${name}`)) {
|
||||
const def = scopes.defs.get(id);
|
||||
if (def === undefined || !isClassLike(def.type) || def.filePath !== owner.filePath) continue;
|
||||
if (picked !== undefined && picked.nodeId !== def.nodeId) return undefined;
|
||||
picked = def;
|
||||
}
|
||||
return picked;
|
||||
};
|
||||
// Namespace CHAIN walk (`ScopeResolver.resolveNamespaceChains`): resolve
|
||||
// every segment of a qualified prefix from its verified namespace root —
|
||||
// or, failing a namespace, from a class binding in scope (`Outer.Inner`).
|
||||
// The cursor is either "these module files" or "this class"; a hop from a
|
||||
// module is a class-like member of it (→ class) or a namespace-import edge
|
||||
// its module scope binds under the segment — a republished module,
|
||||
// `pub const sub = @import("sub.zig");` (→ files); a hop from a class is a
|
||||
// nested class-like. Anything ambiguous resolves nothing.
|
||||
const walkChains = provider.resolveNamespaceChains === true;
|
||||
const namespaceImportTargetsOf = (file: string, name: string): readonly string[] => {
|
||||
const moduleScope = index.moduleScopeByFile.get(file);
|
||||
if (moduleScope === undefined) return [];
|
||||
const out: string[] = [];
|
||||
for (const edge of scopes.imports.get(moduleScope.id) ?? []) {
|
||||
if (edge.kind !== 'namespace' || edge.localName !== name || edge.targetFile === null)
|
||||
continue;
|
||||
if (!out.includes(edge.targetFile)) out.push(edge.targetFile);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
type ChainCursor =
|
||||
| { readonly files: readonly string[] }
|
||||
| { readonly classDef: SymbolDefinition };
|
||||
const resolveNamespaceChain = (
|
||||
prefix: string,
|
||||
inScope: ScopeId,
|
||||
namespaceTargets: ReadonlyMap<string, readonly string[]>,
|
||||
): ChainCursor | undefined => {
|
||||
const segments = splitTopLevelDots(prefix);
|
||||
if (segments.length === 0) return undefined;
|
||||
let cursor: ChainCursor | undefined;
|
||||
let rest: readonly string[] = [];
|
||||
// The LONGEST namespace key wins: a provider may bind dotted handles
|
||||
// (`namespaceReceiverPaths`) and an inline `@import("x.zig")` handle
|
||||
// carries a dot of its own inside the quotes.
|
||||
for (let k = segments.length; k >= 1; k--) {
|
||||
const key = segments.slice(0, k).join('.');
|
||||
const files = namespaceTargets.get(key);
|
||||
if (files === undefined) continue;
|
||||
if (isNamespaceNameShadowed(key, inScope, scopes)) return undefined;
|
||||
cursor = { files };
|
||||
rest = segments.slice(k);
|
||||
break;
|
||||
}
|
||||
if (cursor === undefined) {
|
||||
const head = findClassBindingInScope(inScope, segments[0]!, scopes);
|
||||
if (head === undefined || !isClassLike(head.type)) return undefined;
|
||||
cursor = { classDef: head };
|
||||
rest = segments.slice(1);
|
||||
}
|
||||
for (const segment of rest) {
|
||||
if (segment.includes('(') || segment.includes('[')) return undefined;
|
||||
if ('files' in cursor) {
|
||||
const asClass = uniqueClassAcross(cursor.files, segment);
|
||||
const asModule: string[] = [];
|
||||
for (const file of cursor.files) {
|
||||
for (const target of namespaceImportTargetsOf(file, segment)) {
|
||||
if (!asModule.includes(target)) asModule.push(target);
|
||||
}
|
||||
}
|
||||
if (asClass !== undefined && asModule.length > 0) return undefined; // both — refuse
|
||||
if (asClass !== undefined) cursor = { classDef: asClass };
|
||||
else if (asModule.length > 0) cursor = { files: asModule };
|
||||
else return undefined;
|
||||
} else {
|
||||
const nested = findNestedClass(cursor.classDef, segment);
|
||||
if (nested === undefined) return undefined;
|
||||
cursor = { classDef: nested };
|
||||
}
|
||||
}
|
||||
return cursor;
|
||||
};
|
||||
// `ns.Type` as a receiver, where `ns` is a verified namespace of the current
|
||||
// file and `Type` a class-like member of it — or, with the chain walk, any
|
||||
// `a.b.c.Type` whose prefix resolves. Unique or nothing.
|
||||
const resolveNamespaceQualifiedClass = (
|
||||
receiverName: string,
|
||||
inScope: ScopeId,
|
||||
namespaceTargets: ReadonlyMap<string, readonly string[]>,
|
||||
): SymbolDefinition | undefined => {
|
||||
const dot = walkChains ? lastTopLevelDot(receiverName) : receiverName.lastIndexOf('.');
|
||||
if (dot <= 0 || dot === receiverName.length - 1) return undefined;
|
||||
const head = receiverName.slice(0, dot);
|
||||
const tail = receiverName.slice(dot + 1);
|
||||
if (tail.includes('(') || tail.includes('[')) return undefined;
|
||||
if (walkChains) {
|
||||
const cursor = resolveNamespaceChain(head, inScope, namespaceTargets);
|
||||
if (cursor === undefined) return undefined;
|
||||
return 'classDef' in cursor
|
||||
? findNestedClass(cursor.classDef, tail)
|
||||
: uniqueClassAcross(cursor.files, tail);
|
||||
}
|
||||
const files = namespaceTargets.get(head);
|
||||
if (files === undefined || isNamespaceNameShadowed(head, inScope, scopes)) return undefined;
|
||||
return uniqueClassAcross(files, tail);
|
||||
};
|
||||
const compoundOpts = {
|
||||
fieldFallback,
|
||||
elementTypeOf: provider.elementTypeOf,
|
||||
namespaceExportsIncludeImportedNames: provider.namespaceExportsIncludeImportedNames === true,
|
||||
hoistTypeBindingsToModule,
|
||||
stripReceiverCastExpressions: provider.stripReceiverCastExpressions === true,
|
||||
constructionSyntax: provider.constructionSyntax,
|
||||
|
|
@ -778,7 +966,16 @@ export function emitReceiverBoundCalls(
|
|||
receiverPaths: provider.namespaceReceiverPaths,
|
||||
moduleFileExists: (filePath) => index.moduleScopeByFile.has(filePath),
|
||||
});
|
||||
const fileCompoundOpts = { ...compoundOpts, namespaceTargets };
|
||||
const fileCompoundOpts = {
|
||||
...compoundOpts,
|
||||
namespaceTargets,
|
||||
...(walkChains
|
||||
? {
|
||||
resolveQualifiedClass: (qualifiedName: string, inScope: ScopeId) =>
|
||||
resolveNamespaceQualifiedClass(qualifiedName, inScope, namespaceTargets),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
// Per-file resolved-callee-id capture context (#2227 U2). Built once per
|
||||
// file; `undefined` when the sink is absent (pdg off) so the `tryEmitEdge`
|
||||
// capture is a no-op and emission stays byte-identical (R4).
|
||||
|
|
@ -1265,15 +1462,22 @@ export function emitReceiverBoundCalls(
|
|||
// that is usually empty. Mirrors the order the compound-receiver
|
||||
// construction path already uses.
|
||||
const namespaceCandidates = namespaceTargets.get(receiverName);
|
||||
const targetFiles =
|
||||
let targetFiles: readonly string[] | undefined =
|
||||
namespaceCandidates !== undefined &&
|
||||
!isNamespaceNameShadowed(receiverName, site.inScope, scopes)
|
||||
? namespaceCandidates
|
||||
: undefined;
|
||||
// Chain walk: `hub.sub.helper()` / `hub.sub.Thing{}` — the receiver is
|
||||
// no handle of this file, but its segments reach a module (see
|
||||
// `resolveNamespaceChain`). A prefix that ends in a CLASS is Case 2's.
|
||||
if (targetFiles === undefined && walkChains && lastTopLevelDot(receiverName) > 0) {
|
||||
const cursor = resolveNamespaceChain(receiverName, site.inScope, namespaceTargets);
|
||||
if (cursor !== undefined && 'files' in cursor) targetFiles = cursor.files;
|
||||
}
|
||||
if (targetFiles !== undefined && provider.resolveQualifiedReceiverMember === undefined) {
|
||||
let found = false;
|
||||
for (const targetFile of targetFiles) {
|
||||
const memberDef = findExportedDef(targetFile, memberName, index);
|
||||
const memberDef = lookupNamespaceMember(targetFile, memberName);
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
|
|
@ -1293,7 +1497,15 @@ export function emitReceiverBoundCalls(
|
|||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
// A namespace-qualified construction site (`mod.T{…}`) resolves
|
||||
// here like `mod.fn()` does; the provider's opt-in marker keeps
|
||||
// it distinguishable from an invocation (see
|
||||
// `ScopeResolver.markConstructionSites`).
|
||||
constructionSiteReason(
|
||||
memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
site,
|
||||
provider.markConstructionSites,
|
||||
),
|
||||
seen,
|
||||
0.85,
|
||||
collapse,
|
||||
|
|
@ -1369,7 +1581,16 @@ export function emitReceiverBoundCalls(
|
|||
}
|
||||
|
||||
// ── Case 2: class-name receiver ──────────────────────────────
|
||||
const classDef = findClassBindingInScope(site.inScope, receiverName, scopes);
|
||||
// A namespace-qualified class (`stdx.PRNG.from_seed()`, `terminal
|
||||
// .Terminal.init()`) binds nothing in the caller's scope chain; when the
|
||||
// head names a verified namespace, the tail is looked up as that
|
||||
// module's member — through the same lookup Case 1 / Case 3 use, so a
|
||||
// hub module (a file made only of re-exports) answers when the provider
|
||||
// opted in. Only a bare tail is walked here; `ns.Type.field.m()` is the
|
||||
// compound resolver's shape.
|
||||
const classDef =
|
||||
findClassBindingInScope(site.inScope, receiverName, scopes) ??
|
||||
resolveNamespaceQualifiedClass(receiverName, site.inScope, namespaceTargets);
|
||||
if (classDef !== undefined) {
|
||||
const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
|
|
@ -1413,6 +1634,45 @@ export function emitReceiverBoundCalls(
|
|||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
// `A.Item{}` / `mod.Outer.Inner{}` — a construction whose member is a
|
||||
// type NESTED in the class the receiver names. Neither a method nor a
|
||||
// field, so the owner walk above cannot see it; the chain walk's
|
||||
// nested-class lookup can (`resolveNamespaceChains`).
|
||||
if (memberDef === undefined && walkChains && site.callForm === 'constructor') {
|
||||
const nested = findNestedClass(classDef, memberName);
|
||||
if (nested !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
options.recordResolutionOutcome,
|
||||
parsed.filePath,
|
||||
site,
|
||||
nested,
|
||||
)
|
||||
) {
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
nodeLookup,
|
||||
site,
|
||||
nested,
|
||||
constructionSiteReason(
|
||||
nested.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
site,
|
||||
provider.markConstructionSites,
|
||||
),
|
||||
seen,
|
||||
0.85,
|
||||
collapse,
|
||||
calleeCapture,
|
||||
);
|
||||
if (ok) emitted++;
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
if (
|
||||
suppressDeletedCallTarget(
|
||||
|
|
@ -1455,11 +1715,23 @@ export function emitReceiverBoundCalls(
|
|||
if (typeRef !== undefined && typeRef.rawName.includes('.')) {
|
||||
const [nsName, ...classNameParts] = typeRef.rawName.split('.');
|
||||
const className = classNameParts.join('.');
|
||||
const targetFiles3 = namespaceTargets.get(nsName);
|
||||
// With the chain walk the dotted type is resolved as a whole
|
||||
// (`x: mod.Outer.Inner`, `t: hub.sub.Thing`); the candidate list then
|
||||
// has one entry or none. Without it: the historical one-hop split.
|
||||
const chainDef3 = walkChains
|
||||
? resolveNamespaceQualifiedClass(typeRef.rawName, site.inScope, namespaceTargets)
|
||||
: undefined;
|
||||
const targetFiles3 = walkChains
|
||||
? chainDef3 === undefined
|
||||
? undefined
|
||||
: [chainDef3.filePath]
|
||||
: namespaceTargets.get(nsName);
|
||||
if (targetFiles3 !== undefined && className.length > 0) {
|
||||
let found3 = false;
|
||||
for (const targetFile3 of targetFiles3) {
|
||||
const classDef3 = findExportedDef(targetFile3, className, index);
|
||||
const classDef3 = walkChains
|
||||
? chainDef3
|
||||
: lookupNamespaceMember(targetFile3, className);
|
||||
if (classDef3 !== undefined) {
|
||||
const picked =
|
||||
site.kind === 'call'
|
||||
|
|
@ -1499,7 +1771,15 @@ export function emitReceiverBoundCalls(
|
|||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
// Same marker rule as Case 1 / Case 2: a constructor-form site
|
||||
// reached through a dotted type binding keeps its
|
||||
// ` (constructor)` suffix when the provider opted in; for
|
||||
// every other provider the string is unchanged.
|
||||
constructionSiteReason(
|
||||
memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global',
|
||||
site,
|
||||
provider.markConstructionSites,
|
||||
),
|
||||
seen,
|
||||
// Explicit defaults so the trailing capture ctx (#2227 U2) can
|
||||
// be threaded without changing dedup/confidence behavior.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { cobolScopeResolver } from '../../languages/cobol/scope-resolver.js';
|
|||
import { swiftScopeResolver } from '../../languages/swift/scope-resolver.js';
|
||||
import { dartScopeResolver } from '../../languages/dart/scope-resolver.js';
|
||||
import { vueScopeResolver } from '../../languages/vue/scope-resolver.js';
|
||||
import { zigScopeResolver } from '../../languages/zig/scope-resolver.js';
|
||||
|
||||
/** Map of `SupportedLanguages` → `ScopeResolver`. The scope-resolution phase
|
||||
* iterates this map directly — every registered resolver runs. This is the
|
||||
|
|
@ -51,4 +52,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
|
|||
[SupportedLanguages.Swift, swiftScopeResolver],
|
||||
[SupportedLanguages.Dart, dartScopeResolver],
|
||||
[SupportedLanguages.Vue, vueScopeResolver],
|
||||
[SupportedLanguages.Zig, zigScopeResolver],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1081,6 +1081,7 @@ export function runScopeResolution(
|
|||
{
|
||||
allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
|
||||
constructorCallTargetsClass: provider.constructorCallTargetsClass === true,
|
||||
markConstructionSites: provider.markConstructionSites === true,
|
||||
isFileLocalDef: provider.isFileLocalDef,
|
||||
isBuiltInName: provider.languageProvider.isBuiltInName,
|
||||
freeCallsRequireInstanceOwnership: provider.freeCallsRequireInstanceOwnership === true,
|
||||
|
|
@ -1111,6 +1112,7 @@ export function runScopeResolution(
|
|||
// both correctly emit. See the build site above for why the earlier
|
||||
// allowlist could not be made safe this way.
|
||||
functionLocalValueDefIds,
|
||||
{ markConstructionSites: provider.markConstructionSites === true },
|
||||
);
|
||||
// Last-resort property resolution by workspace-unique name (A1/A5). Runs
|
||||
// after every precise pass and only sees what they left behind, so a
|
||||
|
|
|
|||
|
|
@ -173,9 +173,10 @@ export function namesAtScope(scopeId: ScopeId, scopes: ScopeResolutionIndexes):
|
|||
* that collapses to `@scope.class` in the scope-extractor query contract.
|
||||
*
|
||||
* Semantics widened historically from `'Class' | 'Interface'` to cover
|
||||
* C#-shape languages (struct, record, enum, trait). Languages that emit
|
||||
* only `'Class'` are unaffected — the extra kinds never appear in their
|
||||
* parsed output.
|
||||
* C#-shape languages (struct, record, enum, trait) and Zig tagged unions
|
||||
* (`union(enum)` containers own methods like structs do). Languages that
|
||||
* emit only `'Class'` are unaffected — the extra kinds never appear in
|
||||
* their parsed output.
|
||||
*/
|
||||
export function isClassLike(t: string): boolean {
|
||||
return (
|
||||
|
|
@ -184,7 +185,8 @@ export function isClassLike(t: string): boolean {
|
|||
t === 'Struct' ||
|
||||
t === 'Record' ||
|
||||
t === 'Enum' ||
|
||||
t === 'Trait'
|
||||
t === 'Trait' ||
|
||||
t === 'Union'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -208,12 +210,19 @@ export function isClassLike(t: string): boolean {
|
|||
* `resolveInheritanceBaseInScope` and `resolveQualifiedInheritanceBase` are
|
||||
* (2); receiver typing is (1).
|
||||
*
|
||||
* NOT YET INCLUDED, deliberately: `Typedef` and `Union`. They belong here
|
||||
* conceptually — the `union_item` note on `MEMBER_OWNER_NODE_TYPES` records
|
||||
* the same gap, that a union owns fields captured as `Property` yet is not a
|
||||
* recognized owner — but neither is wired as a member container today, so
|
||||
* adding them would widen a predicate nothing exercises. They join when their
|
||||
* containers do, with fixtures.
|
||||
* `Union` IS included, via `isClassLike`: Zig wires `union(enum)` as a member
|
||||
* container (methods dispatched on a union receiver — see the `main → isEnergy`
|
||||
* case in `test/integration/resolvers/zig.test.ts`), so it is a shape. It
|
||||
* lives in `isClassLike` because that is the label set the ownership walkers
|
||||
* consult, NOT because unions inherit: Zig has no inheritance and its scope
|
||||
* resolver supplies no heritage hooks, so a `Union` never has supertypes and
|
||||
* its MRO is just itself. C/C++ unions still do not emit `Union` defs on the
|
||||
* scope side, so nothing changes for them.
|
||||
*
|
||||
* NOT YET INCLUDED, deliberately: `Typedef`. It belongs here conceptually but
|
||||
* is not wired as a member container today, so adding it would widen a
|
||||
* predicate nothing exercises. It joins when its container does, with
|
||||
* fixtures.
|
||||
*/
|
||||
export function isShapeLike(t: string): boolean {
|
||||
return isClassLike(t) || t === 'TypeAlias';
|
||||
|
|
@ -2104,3 +2113,57 @@ export function findExportedDef(
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `findExportedDef`, then — when the target file declares no such local — a
|
||||
* name the target file IMPORTED and publishes as its own (a hub module).
|
||||
*
|
||||
* A Zig hub is a file made only of re-exports: `pub const Terminal =
|
||||
* @import("Terminal.zig");`, `pub const Thing = @import("thing.zig").Thing;`.
|
||||
* Its module scope owns NO local binding, so `findExportedDef` answers nothing
|
||||
* for `terminal.Terminal.init()` or `t: stdx.Thing`, and the finalized channel
|
||||
* (`lookupBindingsAt`) is the only place the published names exist — origin
|
||||
* `import` / `namespace` / `reexport`, def already resolved to the declaring
|
||||
* file. Measured on ghostty (788 Zig files) before and after this helper:
|
||||
* CALLS into `src/terminal/` from outside that directory went from 46 to 253;
|
||||
* on tigerbeetle, CALLS into its `stdx` hub from outside went from 837 to 1500.
|
||||
*
|
||||
* Opt-in per provider (`ScopeResolver.namespaceExportsIncludeImportedNames`):
|
||||
* in most languages a module's imports are NOT its exports (a TypeScript
|
||||
* `import { X }` publishes nothing), and the finalized edge cannot say whether
|
||||
* the import was written `pub`. Zig opts in because a hub member a consumer
|
||||
* can name through the hub IS public — a private import cannot be reached
|
||||
* through the hub in code that compiles.
|
||||
*
|
||||
* Class-like defs win over anything else bound under the name (a re-exported
|
||||
* type over a same-named value), and a name the finalized channel binds to
|
||||
* several distinct defs is refused — never guess a namespace member.
|
||||
*/
|
||||
export function findExportedDefIncludingImportedNames(
|
||||
targetFile: string,
|
||||
memberName: string,
|
||||
index: WorkspaceResolutionIndex,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
): SymbolDefinition | undefined {
|
||||
const local = findExportedDef(targetFile, memberName, index);
|
||||
if (local !== undefined) return local;
|
||||
const moduleScope = index.moduleScopeByFile.get(targetFile);
|
||||
if (moduleScope === undefined) return undefined;
|
||||
let picked: SymbolDefinition | undefined;
|
||||
for (const ref of lookupBindingsAt(moduleScope.id, memberName, scopes)) {
|
||||
if (ref.origin !== 'import' && ref.origin !== 'namespace' && ref.origin !== 'reexport')
|
||||
continue;
|
||||
if (picked === undefined) {
|
||||
picked = ref.def;
|
||||
continue;
|
||||
}
|
||||
if (picked.nodeId === ref.def.nodeId) continue;
|
||||
if (isClassLike(ref.def.type) && !isClassLike(picked.type)) {
|
||||
picked = ref.def;
|
||||
continue;
|
||||
}
|
||||
if (isClassLike(picked.type) && !isClassLike(ref.def.type)) continue;
|
||||
return undefined; // two distinct defs under one published name → refuse
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2447,6 +2447,167 @@ export const DART_QUERIES = `
|
|||
right: (_)) @assignment
|
||||
`;
|
||||
|
||||
// ── Zig ──────────────────────────────────────────────────────────────────────
|
||||
// Verified against @tree-sitter-grammars/tree-sitter-zig 1.1.2.
|
||||
// Container declarations (struct/enum/union) are anonymous in the grammar; the
|
||||
// binding name lives on the parent variable_declaration's first identifier
|
||||
// child. Heritage queries are intentionally absent — Zig has no inheritance.
|
||||
export const ZIG_QUERIES = `
|
||||
; Functions (top-level + methods inside struct/enum/union containers)
|
||||
(function_declaration
|
||||
name: (identifier) @name) @definition.function
|
||||
|
||||
; Struct: const Foo = struct { ... }
|
||||
(variable_declaration
|
||||
(identifier) @name
|
||||
(struct_declaration)) @definition.struct
|
||||
|
||||
; Enum: const Foo = enum { ... }
|
||||
(variable_declaration
|
||||
(identifier) @name
|
||||
(enum_declaration)) @definition.enum
|
||||
|
||||
; Union: const Foo = union { ... } (and tagged-union union(enum) { ... })
|
||||
(variable_declaration
|
||||
(identifier) @name
|
||||
(union_declaration)) @definition.union
|
||||
|
||||
; File-struct: a file whose top level declares a container field IS a struct
|
||||
; named after the file (\`Page.zig\` declares \`Page\`; \`@typeName\` agrees).
|
||||
; The anchor is the whole file; the name comes from the class extractor
|
||||
; (\`zigContainerName(source_file, filePath)\` — the file stem), not from a
|
||||
; capture, since no node spells it. One match per top-level field — the
|
||||
; definition phase dedupes by (node, name). Namespace-only files (no fields)
|
||||
; never match and keep their Function ids.
|
||||
((source_file (container_field name: (identifier) @_field)) @definition.struct
|
||||
(#not-eq? @_field ""))
|
||||
; A FIELDLESS file-struct — \`Empty.zig\`: no field, but a top-level fn whose
|
||||
; first parameter is typed as the file's own type (\`self: *@This()\`, or
|
||||
; \`self: *Self\` beside \`const Self = @This();\`). Zero-sized types are still
|
||||
; constructed (\`Empty{}\`) and dispatched on, and keyed on fields alone the
|
||||
; file lost its Struct node and every \`e.ping()\` edge (PR #1432 review,
|
||||
; 8.12). The two rules over-match on purpose — any \`@This\` in a first
|
||||
; parameter, any top-level \`@This()\` alias — and the provider's
|
||||
; \`shouldSkipDefinitionCapture\` keeps only what \`isZigFileStruct\` (the
|
||||
; single predicate the owner walk and the scope side use) admits.
|
||||
((source_file (function_declaration (parameters . (parameter type: (_) @_recv))))
|
||||
@definition.struct
|
||||
(#match? @_recv "@This"))
|
||||
((source_file (variable_declaration (identifier) (builtin_function (builtin_identifier) @_this)))
|
||||
@definition.struct
|
||||
(#eq? @_this "@This"))
|
||||
|
||||
; Opaque: const Handle = opaque { ... } — the FFI handle type. It is a
|
||||
; container (it may declare methods, never fields), so it is labelled Struct:
|
||||
; the owner of a HAS_METHOD edge must be class-like, and there is no closer
|
||||
; label. It is NOT a TypeAlias — an opaque type is a distinct nominal type,
|
||||
; deliberately incompatible with whatever it wraps.
|
||||
(variable_declaration
|
||||
(identifier) @name
|
||||
(opaque_declaration)) @definition.struct
|
||||
|
||||
; Generic type constructors: \`pub fn List(comptime T: type) type { return
|
||||
; struct { … }; }\` — Zig's only spelling of a generic type. The returned
|
||||
; container is anonymous in the grammar; the definition anchor is the
|
||||
; container node and its name is the enclosing function's (\`List\`), which
|
||||
; is what every caller writes (\`List(u8)\`). Only the direct \`return
|
||||
; <container>\` of a fn whose return type is \`type\` qualifies (see
|
||||
; \`zigTypeConstructorOf\`). The Function node \`List\` coexists: \`List\` is
|
||||
; both a callable and a type.
|
||||
((function_declaration
|
||||
name: (identifier) @name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(struct_declaration) @definition.struct))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(union_declaration) @definition.union))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(enum_declaration) @definition.enum))))
|
||||
(#eq? @_ret "type"))
|
||||
|
||||
; Function-local and anonymous containers (F8): \`fn string() { const R =
|
||||
; struct { fn get … }; }\` (Lightpanda's reflection.zig declares one \`R\` per
|
||||
; builder fn), \`std.sort.pdq(T, items, {}, struct { fn lessThan … }.lessThan)\`,
|
||||
; \`const cmp = struct { fn lt … }.lt;\`, a field typed \`?struct { min: u32 }\`.
|
||||
; No name child spells their identity, so these rules match EVERY container
|
||||
; and the class extractor names the node from \`zigContainerName\` (\`string$R\`,
|
||||
; \`build$1\`) — the same function the owner walk uses for their fns, which
|
||||
; were ownerless, colliding Methods before. The bound shapes above match too;
|
||||
; the provider's \`shouldSkipDefinitionCapture\` keeps exactly one rule per
|
||||
; container (\`zigContainerAnchor\`).
|
||||
(struct_declaration) @definition.struct
|
||||
(enum_declaration) @definition.enum
|
||||
(union_declaration) @definition.union
|
||||
(opaque_declaration) @definition.struct
|
||||
|
||||
; Container fields (struct fields, enum variants, union variants) — all are
|
||||
; \`container_field\` in the grammar and all become Property (C labels its
|
||||
; enumerators Const; Rust captures no variants; Zig's own vocabulary is
|
||||
; "field" for all three, so one label keeps the query honest).
|
||||
; #not-eq? guard: tree-sitter-zig 1.1.2 recovers an EMPTY container body
|
||||
; (\`struct {}\`, \`opaque {}\`) as a container_field whose identifier is a
|
||||
; zero-width MISSING placeholder — a parser artefact, not a field, and
|
||||
; without the guard it minted a Property with an empty name.
|
||||
((container_field
|
||||
name: (identifier) @name) @definition.property
|
||||
(#not-eq? @name ""))
|
||||
|
||||
; Named tests: test "description" { ... }. The name is the string node WITH
|
||||
; its quotes, so \`test "add"\` next to \`fn add\` (the idiomatic layout) does
|
||||
; not collide on Function:<file>:add. Anonymous \`test {}\` and decl-tests
|
||||
; \`test add {}\` have no name of their own and are not graph nodes; their
|
||||
; bodies' calls attribute to the File.
|
||||
(test_declaration
|
||||
(string) @name) @definition.function
|
||||
|
||||
; const / var bindings that are neither a container nor an @import (those two
|
||||
; are skipped by the provider's \`shouldSkipDefinitionCapture\` so the Struct /
|
||||
; import binding is the only node for that name). The literal keyword is
|
||||
; load-bearing: tree-sitter-zig 1.1.2 parses statement assignments (\`x = 5;\`,
|
||||
; \`x += 1;\`, \`_ = expr;\`) as \`variable_declaration\` WITHOUT a keyword
|
||||
; child, and a keyword-less rule would mint a Const per assignment and a
|
||||
; Variable named \`_\` per discard.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @name) @definition.const
|
||||
(variable_declaration
|
||||
"var" . (identifier) @name) @definition.variable
|
||||
|
||||
; @import("path") — capture the string argument as @import.source, in
|
||||
; EVERY position: the value of a const/var (\`const std = @import("std")\`),
|
||||
; a member chain (\`const X = @import("x.zig").X\`), \`pub usingnamespace
|
||||
; @import("path")\`, a tuple element (\`pub const Interfaces = .{
|
||||
; @import("a.zig"), @import("b.zig") }\`), a call argument, a comparison
|
||||
; operand, the receiver of a member call (\`try @import("dump.zig").root(...)\`).
|
||||
; Zig has no import statement — the builtin IS the import, wherever it sits,
|
||||
; and every occurrence is a file dependency. The #eq? predicate keeps the
|
||||
; other builtins (@sizeOf, @TypeOf, @as, …) out. One rule, one match per
|
||||
; builtin: the structure phase only skips import matches (IMPORTS edges come
|
||||
; from the scope phase — \`emitZigScopeCaptures\`, whose \`@import.inline\`
|
||||
; rule is this rule's twin, decides which occurrences bind a name).
|
||||
((builtin_function
|
||||
(builtin_identifier) @builtin
|
||||
(arguments
|
||||
(string) @import.source))
|
||||
(#eq? @builtin "@import")) @import
|
||||
|
||||
; Free calls: foo(...)
|
||||
(call_expression
|
||||
function: (identifier) @call.name) @call
|
||||
|
||||
; Member calls: obj.method(...) and namespace.fn(...) (e.g. std.debug.print).
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
member: (identifier) @call.name)) @call
|
||||
`;
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
||||
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
||||
|
|
@ -2466,4 +2627,5 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
|||
[SupportedLanguages.Dart]: DART_QUERIES,
|
||||
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
|
||||
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
|
||||
[SupportedLanguages.Zig]: ZIG_QUERIES,
|
||||
};
|
||||
|
|
|
|||
21
gitnexus/src/core/ingestion/type-extractors/zig.ts
Normal file
21
gitnexus/src/core/ingestion/type-extractors/zig.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
|
||||
|
||||
/**
|
||||
* Zig type extraction — minimal v1 stub.
|
||||
*
|
||||
* Type-flow inference (constructor binding, for-loop element types, pattern
|
||||
* binding, etc.) is intentionally out of scope for v1. The provider supplies
|
||||
* an empty `declarationNodeTypes` set and no-op extractors to satisfy the
|
||||
* `LanguageTypeConfig` contract; the central type-env builder will simply
|
||||
* record nothing for Zig files. Receiver resolution today operates without
|
||||
* type-env signal for languages that omit it.
|
||||
*/
|
||||
|
||||
const noopDeclaration: TypeBindingExtractor = () => {};
|
||||
const noopParameter: ParameterExtractor = () => {};
|
||||
|
||||
export const zigTypeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: new Set(),
|
||||
extractDeclaration: noopDeclaration,
|
||||
extractParameter: noopParameter,
|
||||
};
|
||||
|
|
@ -321,6 +321,9 @@ export const FUNCTION_NODE_TYPES = new Set([
|
|||
// Dart
|
||||
'function_signature',
|
||||
'method_signature',
|
||||
// Zig: `test "…" { }` bodies are callable scopes — calls inside attribute
|
||||
// to the test, not the file. Named via methodExtractor.extractFunctionName.
|
||||
'test_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -370,6 +373,9 @@ export const CLASS_CONTAINER_TYPES = new Set([
|
|||
// Go
|
||||
'struct_type',
|
||||
'interface_type',
|
||||
// Zig
|
||||
'union_declaration',
|
||||
'opaque_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -438,6 +444,12 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
|
|||
companion_object: 'Class',
|
||||
struct_type: 'Struct',
|
||||
interface_type: 'Interface',
|
||||
// Zig: tagged and untagged unions are class-like containers, and so is
|
||||
// the fieldless `opaque {}` (may own methods; labelled Struct, see
|
||||
// ZIG_QUERIES). `struct_declaration` and `enum_declaration` are already
|
||||
// present (Dart / generic).
|
||||
union_declaration: 'Union',
|
||||
opaque_declaration: 'Struct',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -581,14 +593,17 @@ export function getLabelFromCaptures(
|
|||
const hasDefaultExportHocNameSeed =
|
||||
captureMap['definition.function'] !== undefined &&
|
||||
(captureMap['hoc'] !== undefined || captureMap['callee'] !== undefined);
|
||||
// Nameless `definition.class` passes through: a class extractor may
|
||||
// synthesize the name (Java anonymous class bodies → `Worker$N`, #2550).
|
||||
// Downstream stays safe — parse-worker skips any nameless definition the
|
||||
// extractor could not name (its `!nameNode && !extractedClassSymbol` gate).
|
||||
// Nameless `definition.class` / `definition.struct` pass through: a class
|
||||
// extractor may synthesize the name (Java anonymous class bodies →
|
||||
// `Worker$N`, #2550; a file-level type named after its file — the
|
||||
// extractor receives the file path for exactly this). Downstream stays
|
||||
// safe — parse-worker skips any nameless definition the extractor could
|
||||
// not name (its `!nameNode && !extractedClassSymbol` gate).
|
||||
if (
|
||||
!captureMap['name'] &&
|
||||
!captureMap['definition.constructor'] &&
|
||||
!captureMap['definition.class'] &&
|
||||
!captureMap['definition.struct'] &&
|
||||
!hasDefaultExportHocNameSeed
|
||||
)
|
||||
return null;
|
||||
|
|
@ -805,6 +820,26 @@ const javaBinaryNameOfType = (node: SyntaxNode): string | undefined => {
|
|||
return enclosingName === undefined ? undefined : `${enclosingName}$${simpleName}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* For a container node that is the direct `return` value of a function whose
|
||||
* declared return type is the literal `type` (`fn List(comptime T: type) type
|
||||
* { return struct {…}; }`), the function's `name` node; undefined otherwise.
|
||||
* Language-agnostic by shape — today only tree-sitter-zig produces it.
|
||||
*/
|
||||
function typeConstructorNameNode(container: SyntaxNode): SyntaxNode | undefined {
|
||||
const ret = container.parent;
|
||||
if (ret?.type !== 'return_expression') return undefined;
|
||||
const stmt = ret.parent;
|
||||
if (stmt?.type !== 'expression_statement') return undefined;
|
||||
const block = stmt.parent;
|
||||
if (block?.type !== 'block') return undefined;
|
||||
const fn = block.parent;
|
||||
if (fn?.type !== 'function_declaration') return undefined;
|
||||
if (fn.childForFieldName?.('body')?.id !== block.id) return undefined;
|
||||
if (fn.childForFieldName?.('type')?.text !== 'type') return undefined;
|
||||
return fn.childForFieldName?.('name') ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authoritative Java local/anonymous type identity.
|
||||
*
|
||||
|
|
@ -879,6 +914,29 @@ export const findEnclosingClassInfo = (
|
|||
* the node-id is built from, guaranteeing owner-id == node-id by construction.
|
||||
*/
|
||||
getQualifiedOwnerName?: (node: SyntaxNode, simpleName: string) => string | null,
|
||||
/**
|
||||
* Optional: the type the whole FILE declares (`LanguageProvider.resolveFileTypeOwner`).
|
||||
* Consulted only when the walk reaches the tree root without meeting a
|
||||
* container, so a member declared at file level can be owned by the file's
|
||||
* own type (Zig file-structs). The name is what the definition phase names
|
||||
* the class-like node, so owner id == node id by construction.
|
||||
*/
|
||||
resolveFileTypeOwner?: (
|
||||
root: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null,
|
||||
/**
|
||||
* Optional: the type a CONTAINER node declares
|
||||
* (`LanguageProvider.resolveContainerTypeOwner`). Consulted for every
|
||||
* `CLASS_CONTAINER_TYPES` node the walk meets, before the generic name-child
|
||||
* derivation, for languages whose containers are named from context (a
|
||||
* binding wrapper, an enclosing callable, an anonymous ordinal). Null falls
|
||||
* through to the generic derivation.
|
||||
*/
|
||||
resolveContainerTypeOwner?: (
|
||||
container: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null,
|
||||
): EnclosingClassInfo | null => {
|
||||
let current = node.parent;
|
||||
let iterations = 0;
|
||||
|
|
@ -976,6 +1034,19 @@ export const findEnclosingClassInfo = (
|
|||
}
|
||||
}
|
||||
|
||||
// A container the PROVIDER names from context (binding wrapper,
|
||||
// enclosing callable, anonymous ordinal — Zig). The name is what the
|
||||
// class-like node is minted under, so owner id == node id.
|
||||
if (resolveContainerTypeOwner !== undefined) {
|
||||
const containerOwner = resolveContainerTypeOwner(current, filePath);
|
||||
if (containerOwner !== null) {
|
||||
return {
|
||||
classId: generateId(containerOwner.label, `${filePath}:${containerOwner.name}`),
|
||||
className: containerOwner.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
|
||||
// NOTE: This impl_item ownership logic is mirrored in
|
||||
// method-extractors/configs/rust.ts (extractOwnerName, metadata only).
|
||||
|
|
@ -1064,7 +1135,27 @@ export const findEnclosingClassInfo = (
|
|||
c.type === 'identifier' ||
|
||||
c.type === 'name' ||
|
||||
c.type === 'constant',
|
||||
);
|
||||
) ??
|
||||
// An ANONYMOUS container bound by the enclosing declaration —
|
||||
// `const Point = struct { … }` (tree-sitter-zig: struct/enum/union/
|
||||
// opaque nodes carry no name; the binding identifier is the first
|
||||
// named child of the parent `variable_declaration`). Same shape as the
|
||||
// Go `type_spec` branch above: the name lives one level up. Without it
|
||||
// the walk climbed past every Zig container and no member ever got a
|
||||
// HAS_METHOD / HAS_PROPERTY owner. The definition phase names the
|
||||
// container node from the same binding (`@name` on the wrapper), so
|
||||
// the owner id and the node id agree by construction.
|
||||
(current.parent?.type === 'variable_declaration'
|
||||
? current.parent.namedChildren?.find((c: SyntaxNode) => c.type === 'identifier')
|
||||
: undefined) ??
|
||||
// An anonymous container RETURNED by a type-constructor function —
|
||||
// `pub fn List(comptime T: type) type { return struct { … }; }`
|
||||
// (Zig's only spelling of a generic type). The container's name is
|
||||
// the function's, which is what the definition phase uses too
|
||||
// (`@name` on the fn identifier, anchor on the container), so the
|
||||
// owner id and the node id agree by construction. Only the literal
|
||||
// `return <container>` of a fn returning `type` qualifies.
|
||||
typeConstructorNameNode(current);
|
||||
if (nameNode) {
|
||||
let label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
|
||||
// Kotlin: class_declaration with an anonymous "interface" keyword child
|
||||
|
|
@ -1121,6 +1212,17 @@ export const findEnclosingClassInfo = (
|
|||
};
|
||||
}
|
||||
}
|
||||
if (current.parent === null && resolveFileTypeOwner !== undefined) {
|
||||
// Tree root reached with no container on the way: ask the provider
|
||||
// whether the file itself is the owner.
|
||||
const fileOwner = resolveFileTypeOwner(current, filePath);
|
||||
if (fileOwner !== null) {
|
||||
return {
|
||||
classId: generateId(fileOwner.label, `${filePath}:${fileOwner.name}`),
|
||||
className: fileOwner.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,15 @@ export interface CallableFlowCaptureOptions {
|
|||
| readonly { readonly destination: SyntaxNode; readonly source: SyntaxNode }[]
|
||||
| undefined;
|
||||
readonly extractFunctionParameters?: (node: SyntaxNode) => readonly SyntaxNode[] | undefined;
|
||||
/**
|
||||
* Provider-owned call-argument extraction, for grammars whose call node
|
||||
* carries its arguments as DIRECT children with no argument-list wrapper
|
||||
* (tree-sitter-zig's `call_expression`). Without a wrapper node the shared
|
||||
* `arguments`/`parameterListNodeTypes` lookup finds nothing, so every
|
||||
* `argument` fact is lost. Returning `undefined` falls back to the shared
|
||||
* path (mirrors `extractFunctionParameters`).
|
||||
*/
|
||||
readonly extractCallArguments?: (call: SyntaxNode) => readonly SyntaxNode[] | undefined;
|
||||
readonly extractCallCallee?: (node: SyntaxNode) => SyntaxNode | undefined;
|
||||
readonly isCallNode?: (node: SyntaxNode) => boolean;
|
||||
/**
|
||||
|
|
@ -173,6 +182,14 @@ interface FunctionInfo {
|
|||
|
||||
interface ValueBindingIndex {
|
||||
readonly assignmentRegionIdsByName: ReadonlyMap<string, ReadonlySet<number>>;
|
||||
/**
|
||||
* Regions holding a store whose destination is a MEMBER path (`o->run =
|
||||
* handler`, `self.f = target`), keyed by the member name. Only these gate a
|
||||
* member call as a field-stored-callable invoke: a plain-name binding
|
||||
* (`const release = deinit;` next to `self.slot.release()`) is not a store
|
||||
* into anybody's member cell.
|
||||
*/
|
||||
readonly memberStoreRegionIdsByName: ReadonlyMap<string, ReadonlySet<number>>;
|
||||
readonly formalByOwner: ReadonlyMap<number | undefined, ReadonlySet<string>>;
|
||||
readonly signatureByNameAndRegion: ReadonlyMap<
|
||||
string,
|
||||
|
|
@ -313,6 +330,7 @@ function buildValueBindingIndex(
|
|||
options: CallableFlowCaptureOptions,
|
||||
): ValueBindingIndex {
|
||||
const assignmentRegionIdsByName = new Map<string, Set<number>>();
|
||||
const memberStoreRegionIdsByName = new Map<string, Set<number>>();
|
||||
const formalByOwner = new Map<number | undefined, Set<string>>();
|
||||
const signatureByNameAndRegion = new Map<string, Map<number, CallableCaptureSignature>>();
|
||||
const add = (
|
||||
|
|
@ -338,19 +356,27 @@ function buildValueBindingIndex(
|
|||
// it (#2522 review, M3 ops-vtable pattern).
|
||||
const terminal = terminalIdentifier(assignment.destination, options);
|
||||
if (terminal !== undefined) destinationNames.add(terminal.text);
|
||||
for (const name of destinationNames) {
|
||||
const region = nearestLexicalRegion(assignment.container, options);
|
||||
let regionIds = assignmentRegionIdsByName.get(name);
|
||||
// A destination whose binding identifier and terminal identifier are two
|
||||
// different nodes spans a member path (`o->run`, `self.slot.f`); a bare
|
||||
// name or a declarator (`void (*fp)(int)`) resolves both to the same leaf.
|
||||
const memberStoreName =
|
||||
terminal !== undefined && terminal.id !== destination?.node.id ? terminal.text : undefined;
|
||||
const region = nearestLexicalRegion(assignment.container, options);
|
||||
const functionOwner =
|
||||
options.functionScopedValueBindings === true
|
||||
? nearestFunctionOwner(assignment.container, options)
|
||||
: undefined;
|
||||
const record = (index: Map<string, Set<number>>, name: string): void => {
|
||||
let regionIds = index.get(name);
|
||||
if (regionIds === undefined) {
|
||||
regionIds = new Set();
|
||||
assignmentRegionIdsByName.set(name, regionIds);
|
||||
index.set(name, regionIds);
|
||||
}
|
||||
regionIds.add(region.id);
|
||||
if (options.functionScopedValueBindings === true) {
|
||||
const functionOwner = nearestFunctionOwner(assignment.container, options);
|
||||
if (functionOwner !== undefined) regionIds.add(functionOwner.id);
|
||||
}
|
||||
}
|
||||
if (functionOwner !== undefined) regionIds.add(functionOwner.id);
|
||||
};
|
||||
for (const name of destinationNames) record(assignmentRegionIdsByName, name);
|
||||
if (memberStoreName !== undefined) record(memberStoreRegionIdsByName, memberStoreName);
|
||||
}
|
||||
for (const fn of functions) {
|
||||
for (const parameter of fn.parameters) {
|
||||
|
|
@ -386,7 +412,12 @@ function buildValueBindingIndex(
|
|||
if (functionOwner !== undefined) byRegion.set(functionOwner.id, signature);
|
||||
}
|
||||
}
|
||||
return { assignmentRegionIdsByName, formalByOwner, signatureByNameAndRegion };
|
||||
return {
|
||||
assignmentRegionIdsByName,
|
||||
memberStoreRegionIdsByName,
|
||||
formalByOwner,
|
||||
signatureByNameAndRegion,
|
||||
};
|
||||
}
|
||||
|
||||
/** True when a pointer/parenthesized declarator sits between the declaration
|
||||
|
|
@ -470,6 +501,34 @@ function isVisibleValueBinding(
|
|||
return visibleCallableSignature(input, name, bindings, options) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member-call gate: the member's name-cell was written by a visible MEMBER
|
||||
* store, or is a declared callable-typed binding (C struct field
|
||||
* `void (*cb)(int);`, whose stores may live in other functions). Plain-name
|
||||
* bindings and formals are deliberately NOT consulted — `x.f()` reads the
|
||||
* member `f` of `x`, not a same-named local, and gating on the local minted an
|
||||
* invoke through the wrong cell (a Zig `pub const release = deinit;` alias
|
||||
* turned `self.slot.release()` into a `deinit → deinit` self-loop).
|
||||
*/
|
||||
function isVisibleMemberStore(
|
||||
input: SyntaxNode,
|
||||
name: string,
|
||||
bindings: ValueBindingIndex,
|
||||
options: CallableFlowCaptureOptions,
|
||||
): boolean {
|
||||
const regionIds = bindings.memberStoreRegionIdsByName.get(name);
|
||||
if (regionIds !== undefined) {
|
||||
const providerOwner = options.lexicalFunctionOwner?.(input);
|
||||
if (providerOwner !== undefined && regionIds.has(providerOwner.id)) return true;
|
||||
let node: SyntaxNode | null = input;
|
||||
while (node !== null) {
|
||||
if (regionIds.has(node.id)) return true;
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
return visibleCallableSignature(input, name, bindings, options) !== undefined;
|
||||
}
|
||||
|
||||
function visibleCallableSignature(
|
||||
input: SyntaxNode,
|
||||
name: string,
|
||||
|
|
@ -700,10 +759,16 @@ function emitCallFacts(
|
|||
const callee = operandSyntax(calleeNode, options);
|
||||
const calleeIsValueBinding =
|
||||
callee !== undefined && isVisibleValueBinding(call, callee.name, valueBindings, options);
|
||||
// A direct callee NAME exists only when the call spells its callee as a
|
||||
// designator (`f(x)`, `ns.f(x)`). A receiver-less member (Zig's decl
|
||||
// literal `.init(x)`, whose receiver is an inferred type) or a computed
|
||||
// callee names nothing the solver may seed by simple name — doing so
|
||||
// fanned each argument out to every same-named callable in the repo.
|
||||
const directCalleeName =
|
||||
member === undefined &&
|
||||
callee !== undefined &&
|
||||
callee.indirection === 0 &&
|
||||
callee.directDesignator &&
|
||||
!calleeIsValueBinding
|
||||
? callee.name
|
||||
: undefined;
|
||||
|
|
@ -816,7 +881,7 @@ function emitCallFacts(
|
|||
// name-keyed field collapse matches the solver's store/load model.
|
||||
// ponytail: same-region joins only — cross-function vtable installs need
|
||||
// a field-sensitive cell model.
|
||||
if (isVisibleValueBinding(call, member.member.name, valueBindings, options)) {
|
||||
if (isVisibleMemberStore(call, member.member.name, valueBindings, options)) {
|
||||
emitInvoke(callSite, member.member, 'indirect', args.length, out, options, member.receiver);
|
||||
}
|
||||
return;
|
||||
|
|
@ -891,6 +956,8 @@ function callArguments(
|
|||
call: SyntaxNode,
|
||||
options: CallableFlowCaptureOptions,
|
||||
): readonly SyntaxNode[] {
|
||||
const providerArguments = options.extractCallArguments?.(call);
|
||||
if (providerArguments !== undefined) return providerArguments;
|
||||
const list =
|
||||
call.childForFieldName('arguments') ??
|
||||
call.childForFieldName('argument') ??
|
||||
|
|
@ -947,10 +1014,18 @@ function memberParts(
|
|||
node.childForFieldName('object') ??
|
||||
node.childForFieldName('argument') ??
|
||||
node.childForFieldName('receiver');
|
||||
// `member` is the field name tree-sitter grammars use when the receiver
|
||||
// field is `object` (Zig `field_expression`). It is only read once a
|
||||
// receiver field matched: the other grammars that expose a `member` field
|
||||
// (C/C++ `offsetof_expression`, JS `class_body`) carry no receiver field and
|
||||
// stay unaffected. Without it every `x.f(arg)` in such a grammar collapsed
|
||||
// to a DIRECT call named `f` and the flow solver fanned the argument out to
|
||||
// every same-named callable.
|
||||
const memberNode =
|
||||
node.childForFieldName('property') ??
|
||||
node.childForFieldName('field') ??
|
||||
node.childForFieldName('method');
|
||||
node.childForFieldName('method') ??
|
||||
node.childForFieldName('member');
|
||||
if (receiverNode === null || memberNode === null) return undefined;
|
||||
const receiver = operandSyntax(receiverNode, options);
|
||||
const member = operandSyntax(memberNode, options);
|
||||
|
|
@ -1062,7 +1137,13 @@ function wrappedExpression(node: SyntaxNode): SyntaxNode | null {
|
|||
node.childForFieldName('expression') ??
|
||||
node.childForFieldName('value');
|
||||
if (field !== null && field.id !== node.id && node.namedChildCount === 1) return field;
|
||||
// `await` is a wrapper, not a callee: tree-sitter-typescript parses
|
||||
// `await f<T>(x)` as `call_expression(function: await_expression(f), …)`,
|
||||
// and `await_expression` carries its operand without a field name, so the
|
||||
// field-based unwrap above misses it. Unwrapping keeps `f` a direct
|
||||
// designator (`direct-callee-name`), as it is for the un-awaited spelling.
|
||||
if (
|
||||
node.type.includes('await_expression') ||
|
||||
node.type.includes('parenthesized') ||
|
||||
node.type.includes('reference_expression') ||
|
||||
node.type.includes('pointer_expression') ||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { VariableExtractionConfig, VariableVisibility } from '../../variable-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { hasZigPubKeyword } from '../../export-detection.js';
|
||||
import {
|
||||
isZigKeywordDeclaration,
|
||||
isZigTypeShadowingBinding,
|
||||
} from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig variable extraction — enriches the Const / Variable nodes minted by
|
||||
* ZIG_QUERIES' `@definition.const` / `@definition.variable` rules.
|
||||
*
|
||||
* tree-sitter-zig uses a single `variable_declaration` node for both `const`
|
||||
* and `var` bindings — AND for statement assignments (`x = 5;`), which carry
|
||||
* no keyword. The keyword is exposed as an unnamed token child
|
||||
* (`type === 'const'` or `type === 'var'`).
|
||||
*
|
||||
* Excludes variable_declarations whose value is a struct/enum/union or an
|
||||
* `@import(...)` builtin — those are handled by the class extractor / import
|
||||
* pipeline, and a Variable record beside the Struct/Enum/Union node (or the
|
||||
* import edge) would be a duplicate. `extractName` returns undefined for them,
|
||||
* which is the generic extractor's skip signal (see `generic.ts`, and Python's
|
||||
* broad `expression_statement` config for the same pattern).
|
||||
*/
|
||||
|
||||
const isVarKeyword = (node: SyntaxNode): boolean => {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child?.type === 'var') return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const zigVariableConfig: VariableExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
constNodeTypes: ['variable_declaration'],
|
||||
staticNodeTypes: [],
|
||||
variableNodeTypes: [],
|
||||
|
||||
extractName(node) {
|
||||
if (isZigTypeShadowingBinding(node)) return undefined;
|
||||
// Statement assignments share the node type and have no keyword — not a
|
||||
// declaration (belt to the query's literal `"const"` / `"var"` brace).
|
||||
if (!isZigKeywordDeclaration(node)) return undefined;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'identifier') return child.text;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// tree-sitter-zig 1.1.2 exposes the annotation as the `type:` field of
|
||||
// variable_declaration (its only field; verified by AST dump: `const p:
|
||||
// *Foo = …` → `pointer_type [type]`, `extern var f: T;` → `identifier
|
||||
// [type]`). Reading the field covers every annotation shape (builtin,
|
||||
// identifier, pointer/optional/slice/array/fn types) and, just as
|
||||
// importantly, never mistakes an initializer for a type: `const f =
|
||||
// target;` has no `type:` field, so it is untyped — the old positional
|
||||
// fallback returned `target` as its type.
|
||||
return node.childForFieldName('type')?.text?.trim();
|
||||
},
|
||||
|
||||
extractVisibility(node): VariableVisibility {
|
||||
// `pub` only — `export var` is C linkage, private to other Zig files.
|
||||
return hasZigPubKeyword(node) ? 'public' : 'private';
|
||||
},
|
||||
|
||||
isConst(node) {
|
||||
return !isVarKeyword(node);
|
||||
},
|
||||
|
||||
isStatic() {
|
||||
return false;
|
||||
},
|
||||
|
||||
isMutable(node) {
|
||||
return isVarKeyword(node);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { parentPort, threadId, workerData } from 'node:worker_threads';
|
||||
import { createRequire } from 'node:module';
|
||||
import {
|
||||
boundCallableStartPosition,
|
||||
localIdentity,
|
||||
|
|
@ -82,6 +83,13 @@ let C: TreeSitterLanguage | null = null;
|
|||
try {
|
||||
C = requireVendoredGrammar('tree-sitter-c') as TreeSitterLanguage;
|
||||
} catch {}
|
||||
|
||||
// @tree-sitter-grammars/tree-sitter-zig is an optionalDependency — may not be installed
|
||||
const _require = createRequire(import.meta.url);
|
||||
let Zig: TreeSitterLanguage | null = null;
|
||||
try {
|
||||
Zig = _require('@tree-sitter-grammars/tree-sitter-zig');
|
||||
} catch {}
|
||||
import { getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import {
|
||||
buildDefinitionPreScan,
|
||||
|
|
@ -561,6 +569,7 @@ const languageMap: Record<string, TreeSitterLanguage> = {
|
|||
[SupportedLanguages.Vue]: TypeScript.typescript,
|
||||
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
|
||||
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
||||
...(Zig ? { [SupportedLanguages.Zig]: Zig } : {}),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -631,6 +640,27 @@ function findEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* `findEnclosingClassNode`, extended with the provider's file-level owner:
|
||||
* when no container encloses `node` but the language says the FILE itself is
|
||||
* a type (`resolveFileTypeOwner`, e.g. a Zig file-struct), the tree root is
|
||||
* the owner node the method/field extractors should read members from. Same
|
||||
* root, same name as `findEnclosingClassInfo`'s root branch, so member ids and
|
||||
* owner ids agree.
|
||||
*/
|
||||
function findEnclosingClassNodeOrFileOwner(
|
||||
node: SyntaxNode,
|
||||
provider: LanguageProvider,
|
||||
filePath: string,
|
||||
): SyntaxNode | null {
|
||||
const container = findEnclosingClassNode(node);
|
||||
if (container !== null) return container;
|
||||
if (provider.resolveFileTypeOwner === undefined) return null;
|
||||
let root: SyntaxNode = node;
|
||||
while (root.parent) root = root.parent;
|
||||
return provider.resolveFileTypeOwner(root, filePath) !== null ? root : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For C++ out-of-class method definitions (e.g. `void Foo::bar() {}`), extract the
|
||||
* class name from the qualified_identifier scope and find the class declaration in the
|
||||
|
|
@ -931,7 +961,14 @@ const callableOwnQualifiedName = (
|
|||
const prefix = enclosingCallablePrefix(fnNode, filePath, provider);
|
||||
const classInfo =
|
||||
prefix === undefined
|
||||
? cachedFindEnclosingClassInfo(fnNode, filePath, provider.resolveEnclosingOwner)
|
||||
? cachedFindEnclosingClassInfo(
|
||||
fnNode,
|
||||
filePath,
|
||||
provider.resolveEnclosingOwner,
|
||||
undefined,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
)
|
||||
: null;
|
||||
const objectOwner =
|
||||
prefix === undefined && classInfo === null && shouldObjectOwnerQualifyCallable(finalLabel)
|
||||
|
|
@ -989,6 +1026,9 @@ const findEnclosingFunctionId = (
|
|||
current,
|
||||
filePath,
|
||||
provider.resolveEnclosingOwner,
|
||||
undefined,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
);
|
||||
const encLang = getLanguageFromFilename(filePath);
|
||||
const standaloneMethodInfo =
|
||||
|
|
@ -1043,8 +1083,12 @@ const findEnclosingFunctionId = (
|
|||
? undefined
|
||||
: standaloneMethodInfo.parameters.length;
|
||||
} else {
|
||||
// Same owner lookup as the definition-phase Method id builder: a Zig
|
||||
// file-struct's top-level fn is owned by the file root, and its
|
||||
// id carries the `#<arity>` suffix only if that owner is found.
|
||||
const classNode =
|
||||
findEnclosingClassNode(current) ?? findClassNodeByQualifiedName(current);
|
||||
findEnclosingClassNodeOrFileOwner(current, provider, filePath) ??
|
||||
findClassNodeByQualifiedName(current);
|
||||
if (classNode && encLang) {
|
||||
const methodMap = getMethodInfo(classNode, provider, {
|
||||
filePath,
|
||||
|
|
@ -1090,6 +1134,9 @@ const findEnclosingFunctionId = (
|
|||
current.previousSibling ?? current,
|
||||
filePath,
|
||||
provider.resolveEnclosingOwner,
|
||||
undefined,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
);
|
||||
// Same nesting rule as the generic branch above (#2699). Anchored on
|
||||
// `sigNode`-equivalent (`current.previousSibling ?? current`) so Dart,
|
||||
|
|
@ -1154,6 +1201,8 @@ const cachedFindEnclosingClassInfo = (
|
|||
filePath: string,
|
||||
resolveEnclosingOwner?: (node: SyntaxNode) => SyntaxNode | null,
|
||||
getQualifiedOwnerName?: (node: SyntaxNode, simpleName: string) => string | null,
|
||||
resolveFileTypeOwner?: LanguageProvider['resolveFileTypeOwner'],
|
||||
resolveContainerTypeOwner?: LanguageProvider['resolveContainerTypeOwner'],
|
||||
): EnclosingClassInfo | null => {
|
||||
const cached = classIdCache.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
|
|
@ -1163,6 +1212,8 @@ const cachedFindEnclosingClassInfo = (
|
|||
filePath,
|
||||
resolveEnclosingOwner,
|
||||
getQualifiedOwnerName,
|
||||
resolveFileTypeOwner,
|
||||
resolveContainerTypeOwner,
|
||||
);
|
||||
classIdCache.set(node, result);
|
||||
return result;
|
||||
|
|
@ -2020,6 +2071,8 @@ const processFileGroup = (
|
|||
file.path,
|
||||
provider.resolveEnclosingOwner,
|
||||
propGetQualifiedOwnerName,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
);
|
||||
const propEnclosingClassId =
|
||||
propEnclosingInfo?.qualifiedClassId ?? propEnclosingInfo?.classId ?? null;
|
||||
|
|
@ -2191,6 +2244,7 @@ const processFileGroup = (
|
|||
? provider.classExtractor.extract(definitionNode, {
|
||||
name: nameNode?.text,
|
||||
type: defaultNodeLabel,
|
||||
filePath: file.path,
|
||||
})
|
||||
: null;
|
||||
const nodeLabel = extractedClassSymbol?.type ?? defaultNodeLabel;
|
||||
|
|
@ -2199,7 +2253,8 @@ const processFileGroup = (
|
|||
nodeLabel === 'Struct' ||
|
||||
nodeLabel === 'Interface' ||
|
||||
nodeLabel === 'Enum' ||
|
||||
nodeLabel === 'Record';
|
||||
nodeLabel === 'Record' ||
|
||||
nodeLabel === 'Union';
|
||||
if (
|
||||
isClassLikeLabel &&
|
||||
provider.classExtractor?.shouldSkipClassCapture?.({
|
||||
|
|
@ -2426,6 +2481,8 @@ const processFileGroup = (
|
|||
file.path,
|
||||
provider.resolveEnclosingOwner,
|
||||
getQualifiedOwnerName,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
)
|
||||
: null;
|
||||
const enclosingClassId =
|
||||
|
|
@ -2641,7 +2698,8 @@ const processFileGroup = (
|
|||
let enrichedByMethodExtractor = false;
|
||||
if (provider.methodExtractor && definitionNode) {
|
||||
const classNode =
|
||||
findEnclosingClassNode(definitionNode) ?? findClassNodeByQualifiedName(definitionNode);
|
||||
findEnclosingClassNodeOrFileOwner(definitionNode, provider, file.path) ??
|
||||
findClassNodeByQualifiedName(definitionNode);
|
||||
if (classNode) {
|
||||
const methodMap = getMethodInfo(classNode, provider, {
|
||||
filePath: file.path,
|
||||
|
|
@ -2840,7 +2898,7 @@ const processFileGroup = (
|
|||
if (nodeLabel === 'Property' && definitionNode) {
|
||||
// FieldExtractor is the single source of truth when available
|
||||
if (provider.fieldExtractor && typeEnv) {
|
||||
const classNode = findEnclosingClassNode(definitionNode);
|
||||
const classNode = findEnclosingClassNodeOrFileOwner(definitionNode, provider, file.path);
|
||||
if (classNode) {
|
||||
const fieldMap = getFieldInfo(classNode, provider, {
|
||||
typeEnv,
|
||||
|
|
|
|||
|
|
@ -494,14 +494,14 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [
|
|||
];
|
||||
|
||||
/**
|
||||
* The 69 pairs NEITHER rule above generates — everything left after the two
|
||||
* The 66 pairs NEITHER rule above generates — everything left after the two
|
||||
* cross products are subtracted. Carried by CONTAINMENT, inheritance, imports
|
||||
* and DI: a container label crossed with a contained label. No predicate
|
||||
* describes that surface (any container can hold any definition).
|
||||
*
|
||||
* What survives here is characteristic, not arbitrary. Almost all of it is a
|
||||
* TARGET no rule reaches — `CodeElement`, `Impl`, `Namespace`, `Template`,
|
||||
* `Typedef`, `Union`, `Static`, `Section`, `Folder` are in neither
|
||||
* `Typedef`, `Static`, `Section`, `Folder` are in neither
|
||||
* `SCOPE_BRIDGE_TARGET_LABELS` nor {@link ATTACHMENT_TARGET_LABELS} — plus the
|
||||
* `Impl|*` and `Template|*` member rows (Rust `impl`/`trait` bodies, C++
|
||||
* templates), the two `Route|Process` / `Tool|Process` entry points whose
|
||||
|
|
@ -529,7 +529,7 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [
|
|||
* from it and so moves with any change to it.
|
||||
*
|
||||
* Folding this remainder into a third cross product
|
||||
* (`DEFINITION_ANCHOR_LABELS × {CodeElement, Section, Typedef, Union,
|
||||
* (`DEFINITION_ANCHOR_LABELS × {CodeElement, Section, Typedef,
|
||||
* Namespace, Impl, TypeAlias, Static, Template}`) would take the table to 641
|
||||
* pairs and leave only ~29 lines here. `bench/schema-pairs` measures 641 at
|
||||
* 1.22–1.43× on the historical reference box; current production's 461 pairs
|
||||
|
|
@ -546,7 +546,6 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [
|
|||
export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
|
||||
FROM File TO CodeElement,
|
||||
FROM File TO \`Typedef\`,
|
||||
FROM File TO \`Union\`,
|
||||
FROM File TO \`Namespace\`,
|
||||
FROM File TO \`Impl\`,
|
||||
FROM File TO \`Static\`,
|
||||
|
|
@ -558,11 +557,9 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
|
|||
FROM Function TO \`Namespace\`,
|
||||
FROM Function TO \`Impl\`,
|
||||
FROM Function TO \`Typedef\`,
|
||||
FROM Function TO \`Union\`,
|
||||
FROM Function TO CodeElement,
|
||||
FROM Class TO \`Template\`,
|
||||
FROM Class TO \`Impl\`,
|
||||
FROM Class TO \`Union\`,
|
||||
FROM Class TO \`Namespace\`,
|
||||
FROM Class TO \`Typedef\`,
|
||||
FROM Class TO CodeElement,
|
||||
|
|
|
|||
|
|
@ -177,6 +177,23 @@ const SOURCES: Record<string, GrammarSource> = {
|
|||
'Likely cause: no prebuilt `.node` for this platform/architecture. ' +
|
||||
`See ${ISSUES_URL}/2107.`,
|
||||
},
|
||||
// Zig grammar declares peerOptional `tree-sitter@^0.22.1` but its native
|
||||
// binding is ABI-compatible with the bundled `tree-sitter@0.21.x` runtime
|
||||
// (verified by load-time smoke test). The peer-dep mismatch is suppressed
|
||||
// via the `overrides` block in package.json. Listed as `optional: true`
|
||||
// because the package is in `optionalDependencies` — users on platforms
|
||||
// without a prebuild may not have it — and `userSkippable` because it is
|
||||
// a genuinely-optional grammar: `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` must be
|
||||
// able to disable it at analyze time like swift/dart/kotlin.
|
||||
[SupportedLanguages.Zig]: {
|
||||
load: () => _require('@tree-sitter-grammars/tree-sitter-zig'),
|
||||
optional: true,
|
||||
userSkippable: true,
|
||||
unavailableNote:
|
||||
'Zig parsing disabled: `@tree-sitter-grammars/tree-sitter-zig` is an ' +
|
||||
'optionalDependency and is not installed (or its native binding failed ' +
|
||||
'to load on this platform).',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -720,7 +720,19 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|||
// ModuleConstants shadow metadata. A warm v90 cache would replay unchanged
|
||||
// Kotlin files with neither route candidates nor the constant declarations
|
||||
// needed to fold them, leaving the new ingestion path silently inert.
|
||||
const SCHEMA_BUMP = 91;
|
||||
// 91 -> 92 (#1432): the shared callable-flow reader (`callable-flow-captures.ts`)
|
||||
// no longer names a callee by simple name for a MEMBER call and gates a
|
||||
// field-stored-callable invoke on a visible member store — parse-time capture
|
||||
// facts for Kotlin / C++ / C# / TypeScript member calls change (the
|
||||
// scope-capture bench re-baselined all four), and Zig files are captured for
|
||||
// the first time, with rules that changed within the PR (qualified struct
|
||||
// literals, enum-variant field bindings, receiver tagging). A warm v91 cache
|
||||
// replays the old facts verbatim, `--force` included: a reviewer re-testing a
|
||||
// later head of this PR on an index built from an earlier one measured a
|
||||
// byte-identical graph until `parse-cache/` and `parsedfile-cache/` were
|
||||
// deleted by hand. 92 is the next free value above origin/main (91) at merge
|
||||
// time. RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING.
|
||||
const SCHEMA_BUMP = 92;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@
|
|||
},
|
||||
"csharp-variadic-resolution/Utils/Logger.cs": {
|
||||
"captureGroups": 10,
|
||||
"digest": "46905fc6f0d59035df05fe60d29c57217d321c5a8d3c3223466061b56c7f785d"
|
||||
"digest": "4907421baf3bbbcb46447b57beb2b1fb29f4ecd1555e79ea08d1b653c636666e"
|
||||
},
|
||||
"csharp-write-access/Models.cs": {
|
||||
"captureGroups": 9,
|
||||
|
|
|
|||
36
gitnexus/test/fixtures/lang-resolution/zig-basic/src/ffi.zig
vendored
Normal file
36
gitnexus/test/fixtures/lang-resolution/zig-basic/src/ffi.zig
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
const std = @import("std");
|
||||
const main_mod = @import("./main.zig");
|
||||
|
||||
// C-ABI export: no `pub`, still the most externally visible symbol in the file.
|
||||
export fn c_add(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Fieldless container: FFI handle type. May own methods, never fields.
|
||||
pub const Handle = opaque {
|
||||
pub fn close(self: *Handle) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
// Empty container body — tree-sitter-zig recovers it with a MISSING
|
||||
// placeholder field; it must not become a nameless Property.
|
||||
pub const Empty = struct {};
|
||||
|
||||
pub fn release(h: *Handle) void {
|
||||
h.close();
|
||||
}
|
||||
|
||||
test "c_add adds" {
|
||||
_ = c_add(1, 2);
|
||||
}
|
||||
|
||||
// The idiomatic layout: a test named after the function it exercises.
|
||||
test "release" {
|
||||
var h: *Handle = undefined;
|
||||
release(h);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = c_add(3, 4);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/zig-basic/src/main.zig
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/zig-basic/src/main.zig
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const std = @import("std");
|
||||
const pioneer = @import("./pioneer.zig");
|
||||
|
||||
pub fn main() void {
|
||||
var p = pioneer.Pioneer{ .energy = 0 };
|
||||
p.tick();
|
||||
helper();
|
||||
const t = pioneer.Tag{ .energy = 5 };
|
||||
_ = t.isEnergy();
|
||||
}
|
||||
|
||||
fn helper() void {
|
||||
_ = 1;
|
||||
}
|
||||
22
gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
pub const State = enum { idle, working };
|
||||
|
||||
pub const Tag = union(enum) {
|
||||
none,
|
||||
energy: u32,
|
||||
|
||||
pub fn isEnergy(self: Tag) bool {
|
||||
return self == .energy;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Pioneer = struct {
|
||||
energy: u32,
|
||||
|
||||
pub fn tick(self: *Pioneer) void {
|
||||
self.energy += 1;
|
||||
}
|
||||
|
||||
pub fn reset(self: *Pioneer) void {
|
||||
self.energy = 0;
|
||||
}
|
||||
};
|
||||
34
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/build.zig
vendored
Normal file
34
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/build.zig
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
const std = @import("std");
|
||||
|
||||
// Two executables that each bind the alias "config" to THEIR OWN config.zig —
|
||||
// the ordinary multi-target layout. A single first-wins map of aliases sends
|
||||
// tool/main.zig's `@import("config")` to app/config.zig.
|
||||
pub fn build(b: *std.Build) void {
|
||||
const corelib = b.dependency("corelib", .{});
|
||||
|
||||
const app_config = b.createModule(.{ .root_source_file = b.path("src/app/config.zig") });
|
||||
const tool_config = b.createModule(.{ .root_source_file = b.path("src/tool/config.zig") });
|
||||
|
||||
const app = b.addExecutable(.{ .name = "app", .root_source_file = b.path("src/app/main.zig") });
|
||||
app.root_module.addImport("config", app_config);
|
||||
// A path dep's NAMED module (declared by libs/corelib/build.zig).
|
||||
app.root_module.addImport("api", corelib.module("core"));
|
||||
|
||||
const tool_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/tool/main.zig"),
|
||||
.imports = &.{ .{ .name = "config", .module = tool_config } },
|
||||
});
|
||||
const tool = b.addExecutable(.{ .name = "tool", .root_module = tool_mod });
|
||||
b.installArtifact(app);
|
||||
b.installArtifact(tool);
|
||||
|
||||
// Two modules rooted in ONE directory that disagree on "clash": a file of
|
||||
// that directory which is neither root cannot be attributed, and must
|
||||
// resolve nothing rather than the first declaration.
|
||||
const clash_a = b.createModule(.{ .root_source_file = b.path("src/shared/clash_a.zig") });
|
||||
const clash_b = b.createModule(.{ .root_source_file = b.path("src/shared/clash_b.zig") });
|
||||
const shared_a = b.addModule("shared_a", .{ .root_source_file = b.path("src/shared/a.zig") });
|
||||
shared_a.addImport("clash", clash_a);
|
||||
const shared_b = b.addModule("shared_b", .{ .root_source_file = b.path("src/shared/b.zig") });
|
||||
shared_b.addImport("clash", clash_b);
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/build.zig.zon
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/build.zig.zon
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.{
|
||||
.name = .buildmodules,
|
||||
.version = "0.1.0",
|
||||
.dependencies = .{
|
||||
.corelib = .{ .path = "libs/corelib" },
|
||||
},
|
||||
.paths = .{ "build.zig", "build.zig.zon", "src" },
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/libs/corelib/build.zig
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/libs/corelib/build.zig
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const std = @import("std");
|
||||
pub fn build(b: *std.Build) void {
|
||||
_ = b.addModule("core", .{ .root_source_file = b.path("src/core.zig") });
|
||||
}
|
||||
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/libs/corelib/src/core.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/libs/corelib/src/core.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub fn ping() void {}
|
||||
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/app/config.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/app/config.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub fn load_app() void {}
|
||||
7
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/app/main.zig
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/app/main.zig
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const config = @import("config");
|
||||
const api = @import("api");
|
||||
|
||||
pub fn main() void {
|
||||
config.load_app();
|
||||
api.ping();
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/a.zig
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/a.zig
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const clash = @import("clash");
|
||||
pub fn use_a() void {
|
||||
clash.hit_a();
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/b.zig
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/b.zig
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const clash = @import("clash");
|
||||
pub fn use_b() void {
|
||||
clash.hit_b();
|
||||
}
|
||||
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/clash_a.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/clash_a.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub fn hit_a() void {}
|
||||
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/clash_b.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/clash_b.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub fn hit_b() void {}
|
||||
5
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/helper.zig
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/shared/helper.zig
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Neither module root: "clash" is ambiguous here and must resolve nothing.
|
||||
const clash = @import("clash");
|
||||
pub fn use_helper() void {
|
||||
clash.hit_a();
|
||||
}
|
||||
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/tool/config.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/tool/config.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub fn load_tool() void {}
|
||||
5
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/tool/main.zig
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/zig-buildmodules/src/tool/main.zig
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const config = @import("config");
|
||||
|
||||
pub fn run_tool() void {
|
||||
config.load_tool();
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/zig-chains/src/Empty.zig
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/zig-chains/src/Empty.zig
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const Self = @This();
|
||||
pub fn ping(self: *Self) void {
|
||||
_ = self;
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/zig-chains/src/Host.zig
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/zig-chains/src/Host.zig
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
n: u32 = 0,
|
||||
pub const Inner = struct {
|
||||
pub fn m(self: Inner) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
pub fn touch(self: *Host) void {
|
||||
_ = self;
|
||||
}
|
||||
const Host = @This();
|
||||
9
gitnexus/test/fixtures/lang-resolution/zig-chains/src/counter.zig
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/zig-chains/src/counter.zig
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub const Counter = struct {
|
||||
n: u32 = 0,
|
||||
pub fn init(n: u32) Counter {
|
||||
return .{ .n = n };
|
||||
}
|
||||
pub fn get(self: Counter) u32 {
|
||||
return self.n;
|
||||
}
|
||||
};
|
||||
1
gitnexus/test/fixtures/lang-resolution/zig-chains/src/hub.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-chains/src/hub.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub const sub = @import("sub.zig");
|
||||
6
gitnexus/test/fixtures/lang-resolution/zig-chains/src/lib.zig
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/zig-chains/src/lib.zig
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub const A = struct {
|
||||
pub fn work() void {}
|
||||
};
|
||||
pub const B = struct {
|
||||
pub fn work() void {}
|
||||
};
|
||||
8
gitnexus/test/fixtures/lang-resolution/zig-chains/src/lists.zig
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/zig-chains/src/lists.zig
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub fn List(comptime T: type) type {
|
||||
return struct {
|
||||
items: []T = &.{},
|
||||
pub fn push(self: *@This()) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
}
|
||||
112
gitnexus/test/fixtures/lang-resolution/zig-chains/src/main.zig
vendored
Normal file
112
gitnexus/test/fixtures/lang-resolution/zig-chains/src/main.zig
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
const counter = @import("counter.zig");
|
||||
const Counter = counter.Counter;
|
||||
const lib = @import("lib.zig");
|
||||
const hub = @import("hub.zig");
|
||||
const nested = @import("nested.zig");
|
||||
const lists = @import("lists.zig");
|
||||
const Empty = @import("Empty.zig");
|
||||
const runner = @import("runner.zig");
|
||||
const Runner = runner.Runner;
|
||||
const opmod = @import("op.zig");
|
||||
const List = lists.List;
|
||||
|
||||
var global_runner = Runner{};
|
||||
var global_runner2: Runner = undefined;
|
||||
|
||||
fn target_global() void {}
|
||||
fn target_global2() void {}
|
||||
fn target_local() void {}
|
||||
fn f_module_receiver() void {
|
||||
global_runner.run(target_global);
|
||||
global_runner2.run(target_global2);
|
||||
}
|
||||
fn f_local_receiver() void {
|
||||
var r = Runner{};
|
||||
r.run(target_local);
|
||||
}
|
||||
|
||||
const chosen = @import("lib.zig").B.work;
|
||||
const chosen2 = lib.B.work;
|
||||
fn f_deep_alias() void {
|
||||
chosen();
|
||||
chosen2();
|
||||
}
|
||||
|
||||
fn f_nested() void {
|
||||
var a = nested.A.Item{};
|
||||
a.run();
|
||||
var b = nested.B.Item{};
|
||||
b.run();
|
||||
}
|
||||
|
||||
fn f_result_location() u32 {
|
||||
const a: Counter = .init(1);
|
||||
const b: Counter = .{ .n = 2 };
|
||||
return a.get() + b.get();
|
||||
}
|
||||
fn f_return_decl_literal() Counter {
|
||||
return .init(3);
|
||||
}
|
||||
|
||||
fn f_sib_a() void {
|
||||
const m = @import("qa.zig");
|
||||
var t = m.Thing{};
|
||||
t.qa_only();
|
||||
m.hello();
|
||||
}
|
||||
fn f_sib_b() void {
|
||||
const m = @import("qb.zig");
|
||||
var t = m.Thing{};
|
||||
t.qb_only();
|
||||
m.hello();
|
||||
}
|
||||
|
||||
fn f_multihop() u32 {
|
||||
var s = hub.sub.Thing{};
|
||||
s.sub_m();
|
||||
var i = nested.Outer.Inner{};
|
||||
i.inner_m();
|
||||
_ = hub.sub.Thing.make();
|
||||
return opmod.Op.lookup.event_max();
|
||||
}
|
||||
|
||||
fn f_inline_generic() void {
|
||||
var t = @import("qa.zig").Thing{};
|
||||
t.qa_only();
|
||||
var l = lists.List(u8){};
|
||||
l.push();
|
||||
var l2 = List(u8){};
|
||||
l2.push();
|
||||
}
|
||||
|
||||
fn f_fieldless() void {
|
||||
var e = Empty{};
|
||||
e.ping();
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
f_module_receiver();
|
||||
f_local_receiver();
|
||||
f_deep_alias();
|
||||
f_nested();
|
||||
_ = f_result_location();
|
||||
_ = f_return_decl_literal();
|
||||
f_sib_a();
|
||||
f_sib_b();
|
||||
_ = f_multihop();
|
||||
f_inline_generic();
|
||||
f_fieldless();
|
||||
}
|
||||
const Host = @import("Host.zig");
|
||||
fn f_filestruct_nested() void {
|
||||
var x = Host.Inner{};
|
||||
x.m();
|
||||
var h = Host{};
|
||||
h.touch();
|
||||
_ = hub.sub.Thing.make();
|
||||
var e2: Empty = .{};
|
||||
e2.ping();
|
||||
}
|
||||
fn f_calls_more() void {
|
||||
f_filestruct_nested();
|
||||
}
|
||||
21
gitnexus/test/fixtures/lang-resolution/zig-chains/src/nested.zig
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/zig-chains/src/nested.zig
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
pub const A = struct {
|
||||
pub const Item = struct {
|
||||
pub fn run(self: Item) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
};
|
||||
pub const B = struct {
|
||||
pub const Item = struct {
|
||||
pub fn run(self: Item) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
};
|
||||
pub const Outer = struct {
|
||||
pub const Inner = struct {
|
||||
pub fn inner_m(self: Inner) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
};
|
||||
7
gitnexus/test/fixtures/lang-resolution/zig-chains/src/op.zig
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/zig-chains/src/op.zig
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub const Op = enum(u8) {
|
||||
create = 1,
|
||||
lookup = 2,
|
||||
pub fn event_max(self: Op) u32 {
|
||||
return @intFromEnum(self);
|
||||
}
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/zig-chains/src/qa.zig
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/zig-chains/src/qa.zig
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub const Thing = struct {
|
||||
pub fn qa_only(self: Thing) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
pub fn hello() void {}
|
||||
6
gitnexus/test/fixtures/lang-resolution/zig-chains/src/qb.zig
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/zig-chains/src/qb.zig
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub const Thing = struct {
|
||||
pub fn qb_only(self: Thing) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
pub fn hello() void {}
|
||||
7
gitnexus/test/fixtures/lang-resolution/zig-chains/src/runner.zig
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/zig-chains/src/runner.zig
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub const Runner = struct {
|
||||
x: u32 = 0,
|
||||
pub fn run(self: *Runner, cb: *const fn () void) void {
|
||||
_ = self;
|
||||
cb();
|
||||
}
|
||||
};
|
||||
8
gitnexus/test/fixtures/lang-resolution/zig-chains/src/sub.zig
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/zig-chains/src/sub.zig
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub const Thing = struct {
|
||||
pub fn sub_m(self: Thing) void {
|
||||
_ = self;
|
||||
}
|
||||
pub fn make() Thing {
|
||||
return .{};
|
||||
}
|
||||
};
|
||||
51
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Page.zig
vendored
Normal file
51
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Page.zig
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// A FILE-STRUCT (Lightpanda `Page.zig` shape): the file has top-level fields,
|
||||
// so it IS a struct named after the file — `Page` — and its top-level fns
|
||||
// taking `self` are its methods. `const Page = @This();` is the idiomatic
|
||||
// self-alias, not a second declaration.
|
||||
const std = @import("std");
|
||||
const Page = @This();
|
||||
const Session = @import("Session.zig");
|
||||
|
||||
session: *Session,
|
||||
count: u32 = 0,
|
||||
|
||||
pub fn init(session: *Session) Page {
|
||||
return .{ .session = session };
|
||||
}
|
||||
|
||||
pub fn getArena(self: *Page) u32 {
|
||||
return self.count;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
// F6 — value flow: a fallible constructor, member calls whose RETURN types
|
||||
// type the local, optionals for `if` / `while` payloads.
|
||||
pub fn make(session: *Session) !*Page {
|
||||
_ = session;
|
||||
return error.Nope;
|
||||
}
|
||||
|
||||
pub fn getSession(self: *Page) *Session {
|
||||
return self.session;
|
||||
}
|
||||
|
||||
pub fn maybeSession(self: *Page) ?*Session {
|
||||
return self.session;
|
||||
}
|
||||
19
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Session.zig
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Session.zig
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const Session = @This();
|
||||
const Page = @import("Page.zig");
|
||||
label: []const u8,
|
||||
|
||||
pub fn name(self: *Session) []const u8 {
|
||||
return self.label;
|
||||
}
|
||||
|
||||
pub fn findFrame(self: *Session, page: *Page) u32 {
|
||||
_ = self;
|
||||
// Method call on a parameter typed by ANOTHER file-struct.
|
||||
return page.getArena();
|
||||
}
|
||||
|
||||
// F6 — an iterator-shaped method: `while (s.next()) |p| p.bump()`.
|
||||
pub fn next(self: *Session) ?*Page {
|
||||
_ = self;
|
||||
return null;
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Sighandler.zig
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Sighandler.zig
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// File-struct whose `@This()` alias is spelled DIFFERENTLY from the file stem
|
||||
// (Lightpanda `Sighandler.zig` / `const SigHandler = @This();`). The type is
|
||||
// still `Sighandler` (what importers write, what `@typeName` says); the alias
|
||||
// is a second name for it inside the file, so `self: *SigHandler` must
|
||||
// resolve to the same Struct.
|
||||
const SigHandler = @This();
|
||||
|
||||
armed: bool = false,
|
||||
|
||||
pub fn arm(self: *SigHandler) void {
|
||||
self.armed = true;
|
||||
self.check();
|
||||
}
|
||||
|
||||
fn check(self: *SigHandler) void {
|
||||
_ = self;
|
||||
}
|
||||
50
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Sorter.zig
vendored
Normal file
50
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Sorter.zig
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// A file-struct hosting ANONYMOUS containers (F8):
|
||||
// - `std.sort.pdq(T, items, {}, struct { fn lessThan … }.lessThan)` — the
|
||||
// comparator idiom (Lightpanda's ImportMap.zig has three in one file, all
|
||||
// named `lessThan`: one ownerless `Method:…:lessThan` node for all three);
|
||||
// - `const byteSize = struct { fn it … }.it;` — build.zig's shape;
|
||||
// - a field typed `?struct { min: u32, max: u32 }`.
|
||||
// Each gets an identity (`Sorter.sortBoth$1`, `Sorter.sortBoth$2`,
|
||||
// `Sorter$1`, `Sorter$2`) so its fns are Methods WITH an owner and never
|
||||
// collide.
|
||||
const std = @import("std");
|
||||
const Sorter = @This();
|
||||
|
||||
items: []u32,
|
||||
bounds: ?struct { min: u32, max: u32 } = null,
|
||||
|
||||
const byteSize = struct {
|
||||
fn it(n: u32) bool {
|
||||
return before(n, n);
|
||||
}
|
||||
}.it;
|
||||
|
||||
pub fn sortBoth(self: *Sorter) void {
|
||||
std.sort.pdq(u32, self.items, {}, struct {
|
||||
fn lessThan(_: void, a: u32, b: u32) bool {
|
||||
return before(a, b);
|
||||
}
|
||||
}.lessThan);
|
||||
std.sort.pdq(u32, self.items, {}, struct {
|
||||
fn lessThan(_: void, a: u32, b: u32) bool {
|
||||
return before(b, a);
|
||||
}
|
||||
}.lessThan);
|
||||
}
|
||||
|
||||
fn before(a: u32, b: u32) bool {
|
||||
return a < b;
|
||||
}
|
||||
|
||||
// A container local to a TEST block (Function.zig / HttpClient.zig tests
|
||||
// declare a `const State = struct {…}` per test): host is the quoted test name.
|
||||
test "Sorter: local state" {
|
||||
const State = struct {
|
||||
n: u32 = 0,
|
||||
fn kill(self: *@This()) void {
|
||||
self.n = if (before(self.n, 1)) 1 else 0;
|
||||
}
|
||||
};
|
||||
var state = State{};
|
||||
state.kill();
|
||||
}
|
||||
85
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/aliases.zig
vendored
Normal file
85
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/aliases.zig
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Type aliases (review finding F7). `const X = <type expr>;` is Zig's only
|
||||
// alias syntax; every one of these is a Const in the graph, and every receiver
|
||||
// below is typed THROUGH the alias. Lightpanda: `pub const Proto = HtmlElement;`,
|
||||
// `const Allocator = std.mem.Allocator;`, `pub const bridge = js.Bridge(T);`.
|
||||
const generic = @import("generic.zig");
|
||||
const Page = @import("Page.zig");
|
||||
const Thing = generic.Thing;
|
||||
|
||||
const Local = struct {
|
||||
pub fn mk() Local {
|
||||
return .{};
|
||||
}
|
||||
pub fn go(self: *Local) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
// b1/b2 — alias of a same-file struct
|
||||
const LocalAlias = Local;
|
||||
// b3/b4 — alias of an alias (the promoted namespace-member import `Thing`)
|
||||
const T2 = Thing;
|
||||
// b5..b7 — alias of an INSTANTIATED generic type constructor
|
||||
const B = generic.List(u8);
|
||||
// alias of a namespace import that is also a file-struct type
|
||||
const P = Page;
|
||||
// a value const and a value call: NOT type aliases
|
||||
const max = 5;
|
||||
const helperResult = generic.Thing.make();
|
||||
|
||||
fn b1() void {
|
||||
_ = LocalAlias.mk();
|
||||
}
|
||||
fn b2() void {
|
||||
var l = LocalAlias.mk();
|
||||
l.go();
|
||||
}
|
||||
fn b3() void {
|
||||
_ = T2.make();
|
||||
}
|
||||
fn b4() void {
|
||||
var t = T2.make();
|
||||
t.run();
|
||||
}
|
||||
fn b5() void {
|
||||
_ = B.init();
|
||||
}
|
||||
fn b6() void {
|
||||
var b = B{};
|
||||
b.push(1);
|
||||
}
|
||||
fn b7() void {
|
||||
var x: B = .{};
|
||||
x.push(2);
|
||||
}
|
||||
// b8 — the same alias inside a fn body (Lightpanda `const R = …;` locals)
|
||||
fn b8() void {
|
||||
const R = generic.List(u16);
|
||||
var r = R.init();
|
||||
r.push(3);
|
||||
}
|
||||
fn b10() void {
|
||||
var q: P = undefined;
|
||||
_ = q.getArena();
|
||||
}
|
||||
// b11 — a VALUE alias (`var cur = orig;`, the cursor idiom) is Rust's
|
||||
// `let x = y`: the same binding, chained to the parameter's type.
|
||||
fn b11(orig: *Local) void {
|
||||
var cur = orig;
|
||||
cur.go();
|
||||
}
|
||||
|
||||
// b9 — Lightpanda's `JsApi` shape: a container-level alias of a forwarding
|
||||
// type constructor, used by every member declaration of the container.
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = generic.Bridge(Local);
|
||||
pub const Meta = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
};
|
||||
pub const value = bridge.accessor("value");
|
||||
};
|
||||
|
||||
pub fn main() void {
|
||||
_ = max;
|
||||
_ = helperResult;
|
||||
}
|
||||
117
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/flow.zig
vendored
Normal file
117
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/flow.zig
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// F6 — the value-flow shapes that bind a local WITHOUT an annotation. Each fn
|
||||
// is one shape; the CALLS edge out of it (`… → bump`, `… → name`) only exists
|
||||
// when the local was typed. Lightpanda: `const page = try Page.init(…)`
|
||||
// (2,551 `try T.f()` sites), `for (self.frames) |*f|`, `if (self.doc) |doc|`,
|
||||
// `while (it.next()) |node|`, `const el = node.asElement()`.
|
||||
const Page = @import("Page.zig");
|
||||
const Session = @import("Session.zig");
|
||||
|
||||
// `try` / `catch` / `orelse` / parens around a constructor call — the
|
||||
// receiver names the type exactly as in the bare `Page.init(…)` shape.
|
||||
fn viaTry(s: *Session) !void {
|
||||
const p = try Page.make(s);
|
||||
p.bump();
|
||||
}
|
||||
fn viaCatch(s: *Session) void {
|
||||
const p = Page.make(s) catch return;
|
||||
p.bump();
|
||||
}
|
||||
fn viaOrelse(p: *Page) void {
|
||||
const s = p.maybeSession() orelse return;
|
||||
_ = s.name();
|
||||
}
|
||||
fn viaParens(s: *Session) !void {
|
||||
const p = (try Page.make(s));
|
||||
p.bump();
|
||||
}
|
||||
fn viaTryLiteral(s: *Session) !void {
|
||||
const p = try Page{ .session = s };
|
||||
p.bump();
|
||||
}
|
||||
|
||||
// A free call: the callee's RETURN type types the local.
|
||||
fn viaFreeCall() void {
|
||||
const p = makeLocal();
|
||||
p.bump();
|
||||
}
|
||||
fn makeLocal() Page {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// A member call on a fn-local receiver: the METHOD's return type, not the
|
||||
// receiver's — `s` is a Session, not a Page.
|
||||
fn viaMemberReturn(p: *Page) void {
|
||||
const s = p.getSession();
|
||||
_ = s.name();
|
||||
}
|
||||
|
||||
// The same on `self`, inside a container (`const el = self.asElement()`).
|
||||
const Runner = struct {
|
||||
page: *Page,
|
||||
pub fn current(self: *Runner) *Page {
|
||||
return self.page;
|
||||
}
|
||||
pub fn go(self: *Runner) void {
|
||||
const p = self.current();
|
||||
p.bump();
|
||||
}
|
||||
};
|
||||
|
||||
// Payload captures.
|
||||
fn forSlice(pages: []Page) void {
|
||||
for (pages) |*p| p.bump();
|
||||
for (pages) |p| {
|
||||
_ = p.getArena();
|
||||
}
|
||||
}
|
||||
fn forIndexed(pages: []Page) void {
|
||||
for (pages, 0..) |p, i| {
|
||||
_ = i;
|
||||
p.bump();
|
||||
}
|
||||
}
|
||||
fn ifOptional(o: ?*Page) void {
|
||||
if (o) |p| p.bump();
|
||||
}
|
||||
fn ifCallOptional(p: *Page) void {
|
||||
if (p.maybeSession()) |s| {
|
||||
_ = s.name();
|
||||
}
|
||||
}
|
||||
fn whileNext(s: *Session) void {
|
||||
while (s.next()) |p| p.bump();
|
||||
}
|
||||
|
||||
// One-layer projections bound to a local.
|
||||
fn viaIndex(pages: []Page) void {
|
||||
const p = pages[0];
|
||||
p.bump();
|
||||
}
|
||||
fn viaUnwrap(o: ?Page) void {
|
||||
const p = o.?;
|
||||
p.bump();
|
||||
}
|
||||
fn viaDeref(ptr: *Page) void {
|
||||
const p = ptr.*;
|
||||
p.bump();
|
||||
}
|
||||
fn viaPtrCaptureDeref(pages: []Page) void {
|
||||
// `|*p|` captures a POINTER: the recorded type must keep the `*`
|
||||
// (`*Page`), or the deref projection below has no layer to remove.
|
||||
for (pages) |*p| {
|
||||
const q = p.*;
|
||||
q.bump();
|
||||
}
|
||||
}
|
||||
|
||||
// Guards: nothing typed here, no edge may appear.
|
||||
fn viaTypeConstructor() void {
|
||||
// A TitleCase callee is a type constructor (an alias, F7), not a value.
|
||||
const L = List(u8);
|
||||
_ = L;
|
||||
}
|
||||
fn List(comptime T: type) type {
|
||||
return struct {
|
||||
x: T,
|
||||
};
|
||||
}
|
||||
38
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/generic.zig
vendored
Normal file
38
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/generic.zig
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// A NAMESPACE file exporting a generic type constructor (`List(u8)`) and the
|
||||
// Lightpanda `js.Bridge(T)` shape: a type constructor that FORWARDS to
|
||||
// another one (`return Builder(T);`) instead of returning a container.
|
||||
pub const Thing = struct {
|
||||
pub fn make() Thing {
|
||||
return .{};
|
||||
}
|
||||
pub fn run(self: *Thing) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn List(comptime T: type) type {
|
||||
return struct {
|
||||
items: []T = &.{},
|
||||
pub fn init() @This() {
|
||||
return .{};
|
||||
}
|
||||
pub fn push(self: *@This(), v: T) void {
|
||||
_ = self;
|
||||
_ = v;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Bridge(comptime T: type) type {
|
||||
return Builder(T);
|
||||
}
|
||||
|
||||
pub fn Builder(comptime T: type) type {
|
||||
return struct {
|
||||
pub fn accessor(comptime name: []const u8) void {
|
||||
_ = name;
|
||||
_ = T;
|
||||
}
|
||||
pub fn prototypeChain() void {}
|
||||
};
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/lp.zig
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/lp.zig
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// A re-export hub (Lightpanda `lightpanda.zig`): `pub const X = @import(...)`
|
||||
// at file scope republishes X, so a third file reaches the TYPE as `lp.X`.
|
||||
pub const Page = @import("Page.zig");
|
||||
pub const Session = @import("Session.zig");
|
||||
const private_util = @import("util.zig");
|
||||
33
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/main.zig
vendored
Normal file
33
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/main.zig
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const std = @import("std");
|
||||
const Page = @import("Page.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Sighandler = @import("Sighandler.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub fn main() void {
|
||||
var s = Session{ .label = "x" };
|
||||
// namespace-member (static) call — must keep working alongside the type
|
||||
var p = Page.init(&s);
|
||||
// receiver typed by the call-return rule (`Page.init` → `Page`)
|
||||
p.bump();
|
||||
_ = p.getArena();
|
||||
_ = s.findFrame(&p);
|
||||
// receiver typed by annotation
|
||||
var q: Page = undefined;
|
||||
_ = q.getArena();
|
||||
var h: Sighandler = .{};
|
||||
h.arm();
|
||||
_ = util.helper();
|
||||
useParam(&p);
|
||||
}
|
||||
|
||||
fn useParam(page: *Page) void {
|
||||
_ = page.getArena();
|
||||
}
|
||||
|
||||
const lp = @import("lp.zig");
|
||||
// The type behind a re-exported name, taken through the hub.
|
||||
const PageViaHub = lp.Page;
|
||||
fn viaHubAlias(page: *PageViaHub) void {
|
||||
_ = page.getArena();
|
||||
}
|
||||
62
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/reflect.zig
vendored
Normal file
62
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/reflect.zig
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Lightpanda's `webapi/element/reflection.zig` shape (F8): a generic type
|
||||
// constructor whose builder fns each declare a FUNCTION-LOCAL helper container
|
||||
// named `R`. By binding name alone every `R` collapsed onto ONE `Struct:…:R`
|
||||
// with one `R.get` / one `R.set`; each must be its own type
|
||||
// (`Reflect.string$R`, `Reflect.url$R`) owning its own methods.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Accessor = struct {
|
||||
get: *const anyopaque,
|
||||
set: *const anyopaque,
|
||||
};
|
||||
|
||||
pub fn Reflect(comptime T: type) type {
|
||||
return struct {
|
||||
pub fn string(comptime attr: []const u8) Accessor {
|
||||
const R = struct {
|
||||
fn get(self: *const T) []const u8 {
|
||||
return readAttr(self, attr);
|
||||
}
|
||||
fn set(self: *T, value: []const u8) void {
|
||||
writeAttr(self, attr, value);
|
||||
}
|
||||
};
|
||||
return Accessor{ .get = R.get, .set = R.set };
|
||||
}
|
||||
|
||||
pub fn url(comptime attr: []const u8) Accessor {
|
||||
const R = struct {
|
||||
// A `@This()` alias inside a function-local container names
|
||||
// THAT container (`R`), not the enclosing type constructor:
|
||||
// `self: *const Self` below must dispatch `self.get()` to
|
||||
// `Reflect.url$R.get`.
|
||||
const Self = @This();
|
||||
fn get(self: *const T) []const u8 {
|
||||
return normalize(readAttr(self, attr));
|
||||
}
|
||||
fn set(self: *T, value: []const u8) void {
|
||||
writeAttr(self, attr, value);
|
||||
}
|
||||
fn check(self: *const Self) []const u8 {
|
||||
return self.get();
|
||||
}
|
||||
};
|
||||
return Accessor{ .get = R.get, .set = R.set };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn readAttr(self: anytype, attr: []const u8) []const u8 {
|
||||
_ = self;
|
||||
return attr;
|
||||
}
|
||||
|
||||
fn writeAttr(self: anytype, attr: []const u8, value: []const u8) void {
|
||||
_ = self;
|
||||
_ = attr;
|
||||
_ = value;
|
||||
}
|
||||
|
||||
fn normalize(s: []const u8) []const u8 {
|
||||
return s;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue