mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(zig): address fourth gitnexus-check review pass
- language-config: `parseZigBuildZon` only accepts a `.path` that is a
DIRECT field of a dependency entry. Nested blocks inside the entry body
are blanked (string-aware, offsets preserved) before the `.path` regex
runs, and a match starting inside a string literal is rejected, so
`.dep = .{ .url = "…", .meta = .{ .path = "x" } }` no longer becomes a
path dep. Regression test in zig-import-resolver.test.ts (fails on the
previous code).
- tree-sitter-languages test: the "grammar is absent" case now drives the
loader's real `source.load()` catch branch — `node:module` is stood in
with a `createRequire` whose require throws MODULE_NOT_FOUND for
`@tree-sitter-grammars/tree-sitter-zig` and delegates everything else —
instead of the `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out, which has its
own test. It also asserts the opt-out flag is NOT set and that other
grammars still load (non-fatal optional failure).
Not re-fixed:
- "Return type is read from the wrong tree-sitter field": tree-sitter-zig
1.1.2 has NO `return_type` field on function_declaration — the type after
`)` is the `type` field (AST dump: `builtin_type "i32" field=type`; the
proposed `childForFieldName('return_type')` is null for every function).
A pin test in zig-extractors.test.ts asserts both the grammar fact and
that `returnType` is extracted (`void`, `!*Counter`).
- "`DIR_LANG` has no zig entry": it does (added in the first pass and
answered again in the third); the finding is carried over unchanged.
This commit is contained in:
parent
b654b66261
commit
4674308b9f
4 changed files with 119 additions and 12 deletions
|
|
@ -574,6 +574,37 @@ function findZonBlockEnd(text: string, start: number): number {
|
|||
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
|
||||
|
|
@ -644,9 +675,19 @@ export function parseZigBuildZon(raw: string): ZigBuildZonConfig | null {
|
|||
const bodyStart = m.index + m[0].length;
|
||||
const bodyEnd = findZonBlockEnd(text, bodyStart);
|
||||
if (bodyEnd < 0 || bodyEnd > end) break;
|
||||
const body = text.slice(bodyStart, bodyEnd);
|
||||
cursor = bodyEnd + 1;
|
||||
const pathMatch = body.match(/\.path\s*=\s*"([^"\n]+)"/);
|
||||
// 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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,23 +715,46 @@ describe('Tree-sitter multi-language parsing', () => {
|
|||
});
|
||||
|
||||
it('reports Zig unavailable and throws "Unsupported language" when the grammar is absent', async () => {
|
||||
// Force the absent-binding path instead of hoping the package is missing:
|
||||
// the loader's runtime opt-out (`GITNEXUS_SKIP_OPTIONAL_GRAMMARS`) makes a
|
||||
// userSkippable grammar report exactly as an absent binding does. A fresh
|
||||
// module copy is needed because the loader memoizes load results.
|
||||
const ENV = 'GITNEXUS_SKIP_OPTIONAL_GRAMMARS';
|
||||
const previous = process.env[ENV];
|
||||
process.env[ENV] = 'zig';
|
||||
// Exercise the loader's real absent-binding branch (the `source.load()`
|
||||
// catch in `loadGrammar`), not the `GITNEXUS_SKIP_OPTIONAL_GRAMMARS`
|
||||
// opt-out, which is a separate code path with its own test
|
||||
// (parser-loader-skip-optional.test.ts). The Zig row loads through the
|
||||
// module-scoped `createRequire(import.meta.url)`, so stand in for
|
||||
// `node:module` with a require that reports the package missing and
|
||||
// delegates everything else. A fresh module copy is needed because the
|
||||
// loader memoizes load results.
|
||||
const ZIG_PKG = '@tree-sitter-grammars/tree-sitter-zig';
|
||||
vi.doMock('node:module', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:module')>();
|
||||
return {
|
||||
...actual,
|
||||
createRequire: (url: string | URL) => {
|
||||
const real = actual.createRequire(url);
|
||||
const stub = ((id: string) => {
|
||||
if (id === ZIG_PKG) {
|
||||
const err = new Error(`Cannot find module '${id}'`) as NodeJS.ErrnoException;
|
||||
err.code = 'MODULE_NOT_FOUND';
|
||||
throw err;
|
||||
}
|
||||
return real(id);
|
||||
}) as unknown as NodeJS.Require;
|
||||
return Object.assign(stub, real);
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.resetModules();
|
||||
try {
|
||||
vi.resetModules();
|
||||
const fresh = await import('../../src/core/tree-sitter/parser-loader.js');
|
||||
// Not the opt-out path: the env is untouched.
|
||||
expect(fresh.isGrammarRuntimeSkipped(SupportedLanguages.Zig)).toBe(false);
|
||||
expect(fresh.isLanguageAvailable(SupportedLanguages.Zig)).toBe(false);
|
||||
await expect(fresh.loadLanguage(SupportedLanguages.Zig)).rejects.toThrow(
|
||||
/Unsupported language/,
|
||||
);
|
||||
// Optional-grammar failure is non-fatal: the other grammars still load.
|
||||
expect(fresh.isLanguageAvailable(SupportedLanguages.TypeScript)).toBe(true);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env[ENV];
|
||||
else process.env[ENV] = previous;
|
||||
vi.doUnmock('node:module');
|
||||
vi.resetModules();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -116,6 +116,28 @@ const Counter = struct {
|
|||
expect(make.isStatic).toBe(true);
|
||||
});
|
||||
|
||||
it('reads the return type from function_declaration’s `type:` field (there is no `return_type`)', () => {
|
||||
// tree-sitter-zig 1.1.2 labels the type after `)` as the `type` field on
|
||||
// function_declaration — the same field NAME parameter nodes use, but on
|
||||
// a different node. There is no `return_type` field: reading that would
|
||||
// drop every Zig return type. Pin both the grammar fact and the extractor.
|
||||
const root = parse(`
|
||||
const Counter = struct {
|
||||
n: u32,
|
||||
pub fn add(self: *Counter, by: u32) void { self.n += by; }
|
||||
pub fn make(n: u32) !*Counter { return error.Nope; }
|
||||
};
|
||||
`).rootNode;
|
||||
const addDecl = find(root, 'function_declaration', 'pub fn add');
|
||||
expect(addDecl.childForFieldName('return_type')).toBeNull();
|
||||
expect(addDecl.childForFieldName('type')?.text).toBe('void');
|
||||
|
||||
const result = extractor.extract(find(root, 'struct_declaration'), ctx);
|
||||
const byName = new Map(result!.methods.map((m) => [m.name, m]));
|
||||
expect(byName.get('add')!.returnType).toBe('void');
|
||||
expect(byName.get('make')!.returnType).toBe('!*Counter');
|
||||
});
|
||||
|
||||
it('only a FIRST parameter named self is the receiver', () => {
|
||||
const root = parse(`
|
||||
const S = struct {
|
||||
|
|
|
|||
|
|
@ -214,6 +214,27 @@ describe('parseZigBuildZon', () => {
|
|||
expect(parseZigBuildZon(raw)).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores a `.path` nested inside an entry — only a direct field makes a path dep', () => {
|
||||
// A URL dep whose body carries a nested object with its own `.path` must
|
||||
// not be reported as a path dep: the resolver would otherwise add an
|
||||
// import edge to an unrelated `<root>/<nested path>` for `@import("only_url")`.
|
||||
const raw = `
|
||||
.{
|
||||
.dependencies = .{
|
||||
.only_url = .{
|
||||
.url = "https://x",
|
||||
.hash = "1220y",
|
||||
.meta = .{ .path = "vendor/unrelated" },
|
||||
},
|
||||
.real = .{ .path = "vendor/real", .extra = .{ .path = "vendor/nested" } },
|
||||
},
|
||||
}
|
||||
`;
|
||||
const cfg = parseZigBuildZon(raw);
|
||||
expect(cfg).not.toBeNull();
|
||||
expect([...cfg!.pathDeps.entries()]).toEqual([['real', 'vendor/real']]);
|
||||
});
|
||||
|
||||
it('returns null when the deps block has no `.path` entries', () => {
|
||||
const raw = `
|
||||
.{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue