fix(zig): address tenth gitnexus-check review pass

- `normalizeZigDepPath`: normalize backslashes BEFORE the absolute-path
  check. A UNC dep (`\\server\share\dep`) used to slip past the check and
  normalize to the repo-relative `server/share/dep`; root-relative `\dep`
  had the same hole. Both now return null. Regression case added to the
  absolute-spellings test with the files those misreadings would resolve.
- `bindPayloads`: a pointer capture `for (pages) |*p|` now records `*Page`
  (declaredSpelling) instead of `Page` — the `*` is an anonymous payload
  child before the identifier. Method dispatch is unchanged (`rawName`
  strips the sigil), but a deref projection `const q = p.*;` now sees the
  pointer layer. New fixture fn `viaPtrCaptureDeref` + assertion; fails on
  the previous code (verified by stashing the src fix).
- `optional-grammar-gate.test.ts`: renamed the `typescript:tsx` case — the
  key IS a registry row; what makes it inert is the missing gate entry. Now
  also asserts a key no registry yields.
- `structural-pair-coverage.test.ts`: header updated — ten tables (not
  eleven) are absent from every rule's target side; `Union` left the set
  when Zig made it linkable.
- `language-classification.ts`: doc comment now names zig in the
  experimental set (added after Ring 1).

Not re-fixed (invalid findings):
- "owner-hook contract wired to an undeclared variable": stale-diff read —
  `findEnclosingClassInfo` declares `resolveFileTypeOwner` /
  `resolveContainerTypeOwner` as optional parameters (ast-helpers.ts:905,
  917) and parse-worker threads them at every call site; tsc compiles clean.
- "optional Zig grammar added unconditionally to the parsing fixture
  suite": the cited block only `fs.readFile`s the committed fixture file to
  assert it is non-empty — no parser or grammar load is involved.
This commit is contained in:
Navid EMAD 2026-08-24 20:38:00 +02:00
parent ef391cc672
commit c740d716be
No known key found for this signature in database
8 changed files with 54 additions and 12 deletions

View file

@ -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';

View file

@ -594,11 +594,14 @@ export async function loadZigBuildConfig(repoRoot: string): Promise<ZigBuildZonC
* resolver so both sides agree on which deps are in-repo.
*/
export function normalizeZigDepPath(depPath: string): string | null {
// POSIX (`/x`) and Windows (`C:\x`, `C:/x`) absolute paths both point
// outside the repository.
if (depPath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(depPath)) return 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 depPath.replace(/\\/g, '/').split('/')) {
for (const part of normalized.split('/')) {
if (part === '' || part === '.') continue;
if (part === '..') {
if (parts.length === 0) return null;

View file

@ -134,8 +134,13 @@ function bindPayloads(node: SyntaxNode, resolver: ZigSubjectTypeResolver): void
if (subject.type === 'range_expression') continue; // `0..` — an index
const spelling = resolver.spellingOf(subject);
if (spelling === undefined) continue;
const element = isFor ? zigElementSpelling(spelling) : zigOptionalPayloadSpelling(spelling);
if (element === 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);

View file

@ -95,6 +95,14 @@ 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 {

View file

@ -611,6 +611,9 @@ describe.skipIf(!zigAvailable)('Zig type aliases (zig-filestruct fixture, aliase
expect(calls).toContain('viaIndex → bump');
expect(calls).toContain('viaUnwrap → bump');
expect(calls).toContain('viaDeref → bump');
// A pointer CAPTURE is deref-able too: `for (pages) |*p|` records `*Page`
// (not `Page`), so `const q = p.*;` still sees the pointer layer.
expect(calls).toContain('viaPtrCaptureDeref → bump');
});
});

View file

@ -6,10 +6,11 @@
* `DEFINITION_ANCHOR_LABELS × ATTACHMENT_TARGET_LABELS` for the framework and
* pipeline-phase overlays so both generated halves are covered there. What
* neither rule can reach is `STRUCTURAL_PAIR_DDL`: the containment, inheritance
* and import pairs BETWEEN TWO DEFINITION LABELS. Eleven node tables are absent
* and import pairs BETWEEN TWO DEFINITION LABELS. Ten node tables are absent
* from every rule's target side (`CodeElement`, `Impl`, `Namespace`,
* `Template`, `TypeAlias`, `Typedef`, `Union`, `Static`, `Section`, `Folder`,
* and the PDG-only `BasicBlock`), so a pair pointing at one is hand-declared or
* `Template`, `TypeAlias`, `Typedef`, `Static`, `Section`, `Folder`, and the
* PDG-only `BasicBlock` `Union` left this set when Zig made it a linkable
* member container), so a pair pointing at one is hand-declared or
* it does not exist. No predicate describes that surface any container can
* hold any definition so this asks the emitters directly: run the real
* pipeline and require every FROM/TO pair it produces to be declared.

View file

@ -60,9 +60,14 @@ describe('optional grammar required-gate', () => {
}
});
it('is inert for a grammar-key variant nobody registered', () => {
// `typescript:tsx` IS a registry key (`listGrammarSources()` yields it) —
// what makes it inert is that OPTIONAL_GRAMMAR_ENV has no entry for it, the
// grammar being mandatory. A key no registry ever yields is inert the same
// way; both stay ungated no matter what env vars are set.
it('is inert for a grammar key with no gate entry, registered or not', () => {
process.env[envVar] = '1';
expect(isOptionalGrammarRequired('typescript:tsx')).toBe(false);
expect(isOptionalGrammarRequired('no-such:grammar')).toBe(false);
});
// Languages with no entry must never be gated — an unregistered language

View file

@ -136,13 +136,27 @@ describe('resolveZigImportInternal', () => {
it('returns null for absolute `.path` deps, POSIX and Windows spellings alike', () => {
// `normalizeZigDepPath` promises null for anything outside the repo; a
// `/`-only check let `C:\\local_dep` through as the relative `C:/local_dep`.
// `/`-only check let `C:\\local_dep` through as the relative `C:/local_dep`,
// and an absolute check that ran BEFORE backslash normalization let the
// UNC `\\\\server\\share\\local_dep` (and root-relative `\\local_dep`)
// through as relative paths. The `root.zig` entries below are the files
// those misreadings WOULD resolve — each spelling must reject, not merely
// miss.
const files = new Set<string>([
'src/main.zig',
'C:/local_dep/src/dep.zig',
'local_dep/src/dep.zig',
'local_dep/src/root.zig',
'server/share/local_dep/src/root.zig',
]);
for (const abs of ['/local_dep', 'C:\\local_dep', 'c:/local_dep', 'D:\\x\\local_dep']) {
for (const abs of [
'/local_dep',
'C:\\local_dep',
'c:/local_dep',
'D:\\x\\local_dep',
'\\\\server\\share\\local_dep',
'\\local_dep',
]) {
const zon = { pathDeps: new Map([['dep', abs]]) };
expect(resolveZigImportInternal('src/main.zig', 'dep', files, zon)).toBeNull();
}