mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
fix(zig): resolve cross-file static gates (#3185)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Scorecard / Scorecard analysis (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
* fix(zig): resolve cross-file static gates * Fix Zig workspace import alias enrichment * Handle extensionless Zig workspace imports * Reuse Zig import resolution for static gates * Document Zig workspace static gating * Harden Zig workspace reference enrichment * Benchmark Zig cross-file static gating * Enforce linear Zig benchmark scaling --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
8f006bd759
commit
1c1cbf111e
12 changed files with 400 additions and 15 deletions
7
.github/workflows/ci-tests.yml
vendored
7
.github/workflows/ci-tests.yml
vendored
|
|
@ -745,6 +745,13 @@ jobs:
|
|||
run: node --import tsx bench/scope-emission/measure.mjs --check
|
||||
working-directory: gitnexus
|
||||
|
||||
- name: Zig cross-file static-gating guards (#3162)
|
||||
if: ${{ !cancelled() }}
|
||||
# Build-free: fingerprints cross-file dead-call classification and
|
||||
# guards the workspace enrichment pass across file-count scaling.
|
||||
run: node --import tsx bench/zig-cross-file-resolution/measure.mjs --check
|
||||
working-directory: gitnexus
|
||||
|
||||
- name: CFG construction time / disk / memory guards (#2081 M1)
|
||||
if: ${{ !cancelled() }}
|
||||
# Build-free: asserts collectFunctionCfgs output is unchanged
|
||||
|
|
|
|||
16
gitnexus/bench/zig-cross-file-resolution/baseline.json
Normal file
16
gitnexus/bench/zig-cross-file-resolution/baseline.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"_comment": "Correctness counts are exact. Timing budgets are deliberately loose and only guard large regressions in the post-extraction Zig workspace pass.",
|
||||
"small": {
|
||||
"modules": 40,
|
||||
"calls_per_module": 12,
|
||||
"gated_calls": 480,
|
||||
"ms_budget": 1000
|
||||
},
|
||||
"large": {
|
||||
"modules": 160,
|
||||
"calls_per_module": 12,
|
||||
"gated_calls": 1920,
|
||||
"ms_budget": 4000
|
||||
},
|
||||
"linear_scaling_slack": 1.375
|
||||
}
|
||||
138
gitnexus/bench/zig-cross-file-resolution/measure.mjs
Normal file
138
gitnexus/bench/zig-cross-file-resolution/measure.mjs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build-free scaling and correctness guard for Zig cross-file static gates.
|
||||
*
|
||||
* The workspace pass parses every indexed Zig file, resolves direct @import
|
||||
* aliases, then applies sibling boolean constants to call sites. This bench
|
||||
* makes both relevant axes explicit: file count and calls per importer. It
|
||||
* also fingerprints the number of calls classified dead, so a fast no-op
|
||||
* implementation cannot pass the timing gate.
|
||||
*
|
||||
* Usage:
|
||||
* node --import tsx bench/zig-cross-file-resolution/measure.mjs
|
||||
* node --import tsx bench/zig-cross-file-resolution/measure.mjs --check
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { populateZigWorkspaceStaticGating } from '../../src/core/ingestion/languages/zig/workspace-static-gating.ts';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SMALL_MODULES = 40;
|
||||
const LARGE_MODULES = 160;
|
||||
const CALLS_PER_MODULE = 12;
|
||||
const REPS = 7;
|
||||
|
||||
const range = (line, col) => ({ startLine: line, startCol: col, endLine: line, endCol: col + 4 });
|
||||
|
||||
function corpus(modules) {
|
||||
const parsedFiles = [];
|
||||
const fileContents = new Map();
|
||||
for (let i = 0; i < modules; i++) {
|
||||
const cfgPath = `bench/cfg${i}.zig`;
|
||||
const appPath = `bench/app${i}.zig`;
|
||||
fileContents.set(cfgPath, 'pub const ENABLED = false;\n');
|
||||
fileContents.set(
|
||||
appPath,
|
||||
`const cfg = @import(\"./cfg${i}.zig\");\n` +
|
||||
Array.from(
|
||||
{ length: CALLS_PER_MODULE },
|
||||
(_, j) => `pub fn run${j}() void { if (cfg.ENABLED) dead${j}(); }`,
|
||||
).join('\n'),
|
||||
);
|
||||
parsedFiles.push(Object.freeze({ filePath: cfgPath, referenceSites: Object.freeze([]) }));
|
||||
parsedFiles.push(
|
||||
Object.freeze({
|
||||
filePath: appPath,
|
||||
referenceSites: Object.freeze(
|
||||
Array.from({ length: CALLS_PER_MODULE }, (_, j) => ({
|
||||
kind: 'call',
|
||||
name: `dead${j}`,
|
||||
atRange: range(j + 2, 44),
|
||||
})),
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return { parsedFiles, fileContents };
|
||||
}
|
||||
|
||||
function run(modules) {
|
||||
const { parsedFiles, fileContents } = corpus(modules);
|
||||
populateZigWorkspaceStaticGating(parsedFiles, { fileContents });
|
||||
let gatedCalls = 0;
|
||||
for (const file of parsedFiles) {
|
||||
for (const site of file.referenceSites) if (site.staticGated === true) gatedCalls++;
|
||||
}
|
||||
return gatedCalls;
|
||||
}
|
||||
|
||||
function measure(modules) {
|
||||
run(modules);
|
||||
let bestMs = Infinity;
|
||||
let gatedCalls = 0;
|
||||
for (let i = 0; i < REPS; i++) {
|
||||
const start = performance.now();
|
||||
gatedCalls = run(modules);
|
||||
bestMs = Math.min(bestMs, performance.now() - start);
|
||||
}
|
||||
return {
|
||||
modules,
|
||||
calls_per_module: CALLS_PER_MODULE,
|
||||
gated_calls: gatedCalls,
|
||||
min_ms: Number(bestMs.toFixed(2)),
|
||||
};
|
||||
}
|
||||
|
||||
const report = { small: measure(SMALL_MODULES), large: measure(LARGE_MODULES) };
|
||||
report.workload_ratio = LARGE_MODULES / SMALL_MODULES;
|
||||
report.scaling_ratio = Number(
|
||||
(report.large.min_ms / Math.max(report.small.min_ms, 0.01)).toFixed(3),
|
||||
);
|
||||
report.linear_factor = Number((report.scaling_ratio / report.workload_ratio).toFixed(3));
|
||||
|
||||
if (!process.argv.includes('--check')) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const baseline = JSON.parse(readFileSync(join(HERE, 'baseline.json'), 'utf8'));
|
||||
const failures = [];
|
||||
const requirePositiveNumber = (path, value) => {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
||||
failures.push(`${path}: expected a finite positive number, got ${JSON.stringify(value)}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
for (const arm of ['small', 'large']) {
|
||||
for (const key of ['modules', 'calls_per_module', 'gated_calls']) {
|
||||
if (report[arm][key] !== baseline[arm][key]) {
|
||||
failures.push(`${arm}.${key}: expected ${baseline[arm][key]}, got ${report[arm][key]}`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
requirePositiveNumber(`${arm}.ms_budget`, baseline[arm].ms_budget) &&
|
||||
report[arm].min_ms > baseline[arm].ms_budget
|
||||
) {
|
||||
failures.push(`${arm}.min_ms ${report[arm].min_ms} exceeds budget ${baseline[arm].ms_budget}`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
requirePositiveNumber('linear_scaling_slack', baseline.linear_scaling_slack) &&
|
||||
report.linear_factor > baseline.linear_scaling_slack
|
||||
) {
|
||||
failures.push(
|
||||
`linear_factor ${report.linear_factor} exceeds slack ${baseline.linear_scaling_slack} ` +
|
||||
`(runtime ${report.scaling_ratio}x for ${report.workload_ratio}x work)`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
if (failures.length > 0) {
|
||||
console.error('[zig-cross-file-resolution --check] FAIL');
|
||||
for (const failure of failures) console.error(` - ${failure}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('[zig-cross-file-resolution --check] PASS');
|
||||
|
|
@ -13,17 +13,14 @@
|
|||
* Conservative by design: we only tag an edge when we can prove the
|
||||
* gating expression evaluates to `false`. Anything ambiguous → live.
|
||||
*
|
||||
* Scope of v1:
|
||||
* Supported scope:
|
||||
*
|
||||
* (a) **File-local** consts (`pub const FOO = false;`, plus const-to-const
|
||||
* aliases up to 5 hops), built once per file by `buildZigBoolConstMap`.
|
||||
* (b) **Cross-file** (`const cfg = @import("./cfg.zig"); if (cfg.FOO)`) is
|
||||
* NOT resolved yet. The evaluator keeps the seam for it (`importAliases`
|
||||
* + `lookupBoolsForPath`, consumed by the `field_expression` case), but
|
||||
* the only caller passes an empty alias map and a lookup that always
|
||||
* returns `undefined`, because the capture emitter runs in the parse
|
||||
* worker and sees only the current file. Tracked in #3162. Until then
|
||||
* every `cfg.FOO` condition folds to unknown, i.e. live.
|
||||
* (b) **Cross-file** direct imports (`const cfg = @import("./cfg.zig");
|
||||
* if (cfg.FOO)`) are enriched after per-file extraction. The workspace
|
||||
* caller supplies `importAliases` and `lookupBoolsForPath`; the parse
|
||||
* worker still uses empty/undefined inputs and remains file-local.
|
||||
*
|
||||
* Also out of scope: multi-hop member access (`cfg.sub.FOO`), re-exported
|
||||
* consts, runtime-evaluated bools (`const FOO = computeIt();`), and
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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';
|
||||
import { populateZigWorkspaceStaticGating } from './workspace-static-gating.js';
|
||||
|
||||
export const zigScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Zig,
|
||||
|
|
@ -67,6 +68,8 @@ export const zigScopeResolver: ScopeResolver = {
|
|||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
populateWorkspaceReferences: populateZigWorkspaceStaticGating,
|
||||
|
||||
// Payload captures — `for (items) |it|`, `if (opt) |v|`, `while (it.next())
|
||||
// |x|` — typed from the subject's binding after finalize (F6).
|
||||
populateRangeBindings: populateZigRangeBindings,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import type { ParsedFile, ReferenceSite } from 'gitnexus-shared';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import type { ZigBuildZonConfig } from '../../language-config.js';
|
||||
import { resolveZigImportInternal } from '../../import-resolvers/zig.js';
|
||||
import {
|
||||
buildZigBoolConstMap,
|
||||
collectZigStaticGatedRanges,
|
||||
isPositionStaticGated,
|
||||
type ZigImportAliasMap,
|
||||
} from '../../call-extractors/zig-static-gating.js';
|
||||
import { parseSourceSafe, ParseTimeoutError } from '../../../tree-sitter/safe-parse.js';
|
||||
import { getZigParser } from './query.js';
|
||||
|
||||
type ZigTree = ReturnType<ReturnType<typeof getZigParser>['parse']>;
|
||||
|
||||
export function populateZigWorkspaceStaticGating(
|
||||
parsedFiles: ParsedFile[],
|
||||
ctx: {
|
||||
readonly fileContents: ReadonlyMap<string, string>;
|
||||
readonly treeCache?: { get(filePath: string): unknown };
|
||||
readonly resolutionConfig?: unknown;
|
||||
},
|
||||
): void {
|
||||
const parser = getZigParser();
|
||||
const trees = new Map<string, ZigTree>();
|
||||
const bools = new Map<string, ReturnType<typeof buildZigBoolConstMap>>();
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const source = ctx.fileContents.get(parsed.filePath);
|
||||
if (source === undefined) continue;
|
||||
let tree = ctx.treeCache?.get(parsed.filePath) as ZigTree | undefined;
|
||||
if (tree === undefined) {
|
||||
try {
|
||||
tree = parseSourceSafe(parser, source, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(source),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ParseTimeoutError) continue;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
trees.set(parsed.filePath, tree);
|
||||
bools.set(parsed.filePath, buildZigBoolConstMap(tree.rootNode));
|
||||
}
|
||||
|
||||
const knownPaths = new Set(trees.keys());
|
||||
for (const [index, parsed] of parsedFiles.entries()) {
|
||||
const tree = trees.get(parsed.filePath);
|
||||
if (tree === undefined) continue;
|
||||
const aliases = collectImportAliases(
|
||||
tree,
|
||||
parsed.filePath,
|
||||
knownPaths,
|
||||
ctx.resolutionConfig as ZigBuildZonConfig | null | undefined,
|
||||
);
|
||||
if (aliases.size === 0) continue;
|
||||
const ranges = collectZigStaticGatedRanges(
|
||||
tree.rootNode,
|
||||
bools.get(parsed.filePath) ?? new Map(),
|
||||
aliases,
|
||||
(filePath) => bools.get(filePath),
|
||||
);
|
||||
if (ranges.length === 0) continue;
|
||||
const next = parsed.referenceSites.map((site) =>
|
||||
site.kind === 'call' &&
|
||||
site.staticGated !== true &&
|
||||
isPositionStaticGated(site.atRange.startLine, site.atRange.startCol, ranges)
|
||||
? ({ ...site, staticGated: true } satisfies ReferenceSite)
|
||||
: site,
|
||||
);
|
||||
parsedFiles[index] = Object.freeze({ ...parsed, referenceSites: Object.freeze(next) });
|
||||
}
|
||||
}
|
||||
|
||||
function collectImportAliases(
|
||||
tree: ZigTree,
|
||||
fromFile: string,
|
||||
knownPaths: ReadonlySet<string>,
|
||||
resolutionConfig?: ZigBuildZonConfig | null,
|
||||
): ZigImportAliasMap {
|
||||
const candidates = new Map<string, string>();
|
||||
const declarationCounts = new Map<string, number>();
|
||||
for (const decl of tree.rootNode.descendantsOfType('variable_declaration')) {
|
||||
const names = decl.namedChildren.filter((node) => node.type === 'identifier');
|
||||
const binding = names[0]?.text;
|
||||
if (binding === undefined) continue;
|
||||
declarationCounts.set(binding, (declarationCounts.get(binding) ?? 0) + 1);
|
||||
const builtin = decl.namedChildren.find(
|
||||
(node) => node.type === 'builtin_function' && node.text.startsWith('@import('),
|
||||
);
|
||||
const raw = builtin?.descendantsOfType('string').at(0)?.text;
|
||||
if (raw === undefined) continue;
|
||||
const specifier = raw.replace(/^['"]|['"]$/g, '');
|
||||
const target = resolveZigImportInternal(fromFile, specifier, knownPaths, resolutionConfig);
|
||||
if (target !== null) candidates.set(binding, target);
|
||||
}
|
||||
|
||||
const aliases = new Map<string, string>();
|
||||
for (const [binding, target] of candidates) {
|
||||
// Alias lookup below is name-based rather than position-aware. If a name
|
||||
// is redeclared in another lexical scope, fail open instead of applying
|
||||
// either module's constants to every use of that spelling.
|
||||
if (declarationCounts.get(binding) === 1) aliases.set(binding, target);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
|
@ -697,6 +697,21 @@ export interface ScopeResolver {
|
|||
ctx: { readonly fileContents: ReadonlyMap<string, string> },
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Optional workspace-wide enrichment of extracted reference sites. Runs
|
||||
* after all files have been extracted and before reference finalization.
|
||||
* Use this when a per-file capture needs conservative facts from an
|
||||
* imported sibling (for example a compile-time branch constant).
|
||||
*/
|
||||
readonly populateWorkspaceReferences?: (
|
||||
parsedFiles: ParsedFile[],
|
||||
ctx: {
|
||||
readonly fileContents: ReadonlyMap<string, string>;
|
||||
readonly treeCache?: { get(filePath: string): unknown };
|
||||
readonly resolutionConfig?: unknown;
|
||||
},
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Recognize a `super(...)`-style receiver text. Python returns
|
||||
* `/^super\s*\(/.test(t)`. Java returns `t === 'super'`. C++ may
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ export function selectScopeSourcePathsToRead(
|
|||
): string[] {
|
||||
const hasPostExtractHooks =
|
||||
provider.populateWorkspaceOwners !== undefined ||
|
||||
provider.populateWorkspaceReferences !== undefined ||
|
||||
provider.populateNamespaceSiblings !== undefined ||
|
||||
provider.populateRangeBindings !== undefined ||
|
||||
provider.emitPostResolutionEdges !== undefined;
|
||||
|
|
|
|||
|
|
@ -639,6 +639,11 @@ export function runScopeResolution(
|
|||
`lang=${provider.language} parsedFiles=${parsedFiles.length} preExtractedHits=${preExtractedHits} skipped=${filesSkipped}`,
|
||||
);
|
||||
provider.populateWorkspaceOwners?.(parsedFiles, { fileContents: getFileContents() });
|
||||
provider.populateWorkspaceReferences?.(parsedFiles, {
|
||||
fileContents: getFileContents(),
|
||||
treeCache,
|
||||
resolutionConfig: input.resolutionConfig,
|
||||
});
|
||||
|
||||
// A callable-flow-only provider has no reason to build the whole-graph
|
||||
// lookup or finalize ordinary references when none of its files emitted a
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
|
||||
// Cross-file alias — should resolve `cfg.FOO`, `cfg.BAR` against cfg.zig.
|
||||
const cfg = @import("./cfg.zig");
|
||||
const cfg_no_ext = @import("./cfg");
|
||||
const wrapped_cfg = wrap(@import("./cfg.zig"));
|
||||
const shadowed_cfg = @import("./cfg.zig");
|
||||
|
||||
pub const UPGRADERS_ENABLED: bool = false;
|
||||
pub const DEBUG: bool = true;
|
||||
|
|
@ -30,6 +33,7 @@ pub const CYCLE_B = CYCLE_A;
|
|||
pub const ALIAS_TO_VAR = IS_RUNTIME_FLAG_FALSE;
|
||||
|
||||
pub fn run() void {
|
||||
const local_cfg = @import("./cfg.zig");
|
||||
// Live: not under any if-gate.
|
||||
live_unconditional();
|
||||
|
||||
|
|
@ -162,6 +166,18 @@ pub fn run() void {
|
|||
if (cfg.NOT_A_BOOL != 0) {
|
||||
live_cross_file_not_bool();
|
||||
}
|
||||
if (cfg_no_ext.FOO) {
|
||||
gated_extensionless_cross_file_foo();
|
||||
}
|
||||
// Function-local imports use the same workspace constants.
|
||||
if (local_cfg.FOO) {
|
||||
gated_local_cross_file_foo();
|
||||
}
|
||||
// An import nested inside another initializer does not bind the variable
|
||||
// directly to that module, so its members must remain unknown/fail-open.
|
||||
if (wrapped_cfg.FOO) {
|
||||
live_wrapped_cross_file_foo();
|
||||
}
|
||||
|
||||
// Bare literal gate: no constant table involved, but it must still be gated.
|
||||
if (false) {
|
||||
|
|
@ -205,10 +221,37 @@ pub fn run() void {
|
|||
_ = e3;
|
||||
}
|
||||
|
||||
pub fn run_shadowed_alias() void {
|
||||
const shadowed_cfg = @import("./other.zig");
|
||||
if (shadowed_cfg.FOO) {
|
||||
live_shadowed_cross_file_foo();
|
||||
}
|
||||
}
|
||||
|
||||
fn live_unconditional() void {
|
||||
_ = 1;
|
||||
}
|
||||
|
||||
fn wrap(value: anytype) @TypeOf(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
fn gated_local_cross_file_foo() void {
|
||||
_ = 1;
|
||||
}
|
||||
|
||||
fn gated_extensionless_cross_file_foo() void {
|
||||
_ = 1;
|
||||
}
|
||||
|
||||
fn live_wrapped_cross_file_foo() void {
|
||||
_ = 1;
|
||||
}
|
||||
|
||||
fn live_shadowed_cross_file_foo() void {
|
||||
_ = 1;
|
||||
}
|
||||
|
||||
fn gated_simple() void {
|
||||
_ = 1;
|
||||
}
|
||||
|
|
|
|||
1
gitnexus/test/fixtures/lang-resolution/zig-static-gating/src/other.zig
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/zig-static-gating/src/other.zig
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub const FOO: bool = true;
|
||||
|
|
@ -7,8 +7,11 @@
|
|||
* such branches keep `staticGated` falsy.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js';
|
||||
import { populateZigWorkspaceStaticGating } from '../../../src/core/ingestion/languages/zig/workspace-static-gating.js';
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
|
||||
describe('Zig static-gated edges', () => {
|
||||
let result: PipelineResult;
|
||||
|
|
@ -174,12 +177,9 @@ describe('Zig static-gated edges', () => {
|
|||
expect(isGated('gated_chain_tail')).toBe(true);
|
||||
});
|
||||
|
||||
// Cross-file positive cases: the gating module resolves `alias.NAME` through
|
||||
// `lookupBoolsForPath`, but the scope-capture emitter runs per file in the
|
||||
// parse worker with only `{ path, content }` in hand — no sibling sources —
|
||||
// so v1 stamps file-local constants only. Re-enable once the emitter can
|
||||
// see imported files (see PR description, "Cross-file constants").
|
||||
it.skip('tags `if (cfg.FOO)` cross-file when FOO is false in cfg.zig (tracked: #3162)', () => {
|
||||
// Cross-file cases are enriched after per-file extraction, once sibling
|
||||
// source facts are available but before reference finalization.
|
||||
it('tags `if (cfg.FOO)` cross-file when FOO is false in cfg.zig', () => {
|
||||
expect(isGated('gated_cross_file_foo')).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ describe('Zig static-gated edges', () => {
|
|||
expect(isGated('live_cross_file_bar')).toBe(false);
|
||||
});
|
||||
|
||||
it.skip('tags the ELSE branch of `if (cfg.BAR)` when BAR is true (tracked: #3162)', () => {
|
||||
it('tags the ELSE branch of `if (cfg.BAR)` when BAR is true', () => {
|
||||
expect(isGated('gated_cross_file_else')).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -198,4 +198,57 @@ describe('Zig static-gated edges', () => {
|
|||
it('does NOT tag `cfg.NOT_A_BOOL != 0` (imported decl is not a bool literal)', () => {
|
||||
expect(isGated('live_cross_file_not_bool')).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves a relative cross-file import with an omitted .zig extension', () => {
|
||||
expect(isGated('gated_extensionless_cross_file_foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('tags a cross-file bool accessed through a function-local import alias', () => {
|
||||
expect(isGated('gated_local_cross_file_foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('does NOT treat a nested @import as the declaration direct module alias', () => {
|
||||
expect(isGated('live_wrapped_cross_file_foo')).toBe(false);
|
||||
});
|
||||
|
||||
it('fails open when an import alias is shadowed in another lexical scope', () => {
|
||||
expect(isGated('live_shadowed_cross_file_foo')).toBe(false);
|
||||
});
|
||||
|
||||
it('replaces a frozen parsed file instead of mutating it', () => {
|
||||
const site = Object.freeze({
|
||||
kind: 'call',
|
||||
atRange: { startLine: 153, startCol: 8, endLine: 153, endCol: 30 },
|
||||
staticGated: false,
|
||||
});
|
||||
const original = Object.freeze({
|
||||
filePath: 'src/main.zig',
|
||||
referenceSites: Object.freeze([site]),
|
||||
}) as unknown as ParsedFile;
|
||||
const parsedFiles = [
|
||||
original,
|
||||
Object.freeze({
|
||||
filePath: 'src/cfg.zig',
|
||||
referenceSites: Object.freeze([]),
|
||||
}) as unknown as ParsedFile,
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
populateZigWorkspaceStaticGating(parsedFiles, {
|
||||
fileContents: new Map([
|
||||
[
|
||||
'src/main.zig',
|
||||
fs.readFileSync(path.join(FIXTURES, 'zig-static-gating', 'src', 'main.zig'), 'utf8'),
|
||||
],
|
||||
[
|
||||
'src/cfg.zig',
|
||||
fs.readFileSync(path.join(FIXTURES, 'zig-static-gating', 'src', 'cfg.zig'), 'utf8'),
|
||||
],
|
||||
]),
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(parsedFiles[0]).not.toBe(original);
|
||||
expect(Object.isFrozen(parsedFiles[0])).toBe(true);
|
||||
expect(parsedFiles[0]?.referenceSites[0]?.staticGated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue