mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
* feat(zig): static-gating analysis module + fixture (ported from feat/zig-static-gated-edges-v2) Squashes c6fe922c, 2f3c8e9e, fab088f4, 9b58af74, aef2ae83, 86b892ef, d5657861, f3780b3a: file-local comptime bool constants, cross-file flag resolution via the @import alias map, re-aliased const chains, == / != against known bools, else / else-if branch awareness. The module is self-contained; the hooks that call it land in the next commit. * feat(zig): stamp static-gated call sites through the scope pipeline Wires the ported gating module into the scope-resolution pipeline that now emits every Zig CALLS edge (PR #1432), replacing the parse-worker / call-processor hooks of the original branch, which targeted the legacy DAG path the merged provider no longer uses. Data flow, one new fact carried end to end: emitZigScopeCaptures stamps `@reference.static-gated` on a call capture whose anchor lies in a statically dead range (body of `if (CONST_FALSE)`, else of `if (CONST_TRUE)`), via the module's new `collectZigStaticGatedRanges` (line/col ranges, because a Capture keeps no node) scope-extractor marker -> `ReferenceSite.staticGated` buildReference -> `Reference.staticGated` references-to-edges, -> `GraphRelationship.staticGated` on the free-call-fallback, emitted CALLS edge (both emit paths, plus edges.ts (tryEmitEdge*) the generic bridge) local-backend impact -> `staticGated` on impact frontier edges Same marker idiom as Go's `@reference.callee-position` / `embedded-pointer`: zero-range, present or absent, so every ungated site's capture set is byte-identical and no other language changes. SCHEMA_BUMP 92 -> 93: parse-time captures changed. Cross-file constants (`if (cfg.FOO)` with `cfg = @import("cfg.zig")`) are NOT stamped yet: the module resolves them through `lookupBoolsForPath`, but the capture emitter runs per file in the parse worker with only `{ path, content }`, so it cannot see the sibling source. The two positive cross-file cases in zig-static-gating.test.ts are `it.skip` with that reason; the negative ones pass unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * feat(graph): add staticGated edge property Adds an optional `staticGated?: boolean` field to `GraphRelationship` that flags edges originating in code branches known at index time to be unreachable in production — e.g. `if (CONST_FALSE)` blocks where the condition reduces to a comptime-known `false`. Schema + persistence wiring: - `gitnexus-shared/src/graph/types.ts` — additive optional field on `GraphRelationship`; absent edges read identically to live ones. - `gitnexus/src/core/lbug/schema.ts` — `staticGated BOOLEAN` column on the `CodeRelation` REL table. - `gitnexus/src/core/lbug/csv-generator.ts` — appends a `staticGated` column (0/1) to the `relations.csv` written for bulk COPY ingest. - `gitnexus/src/core/lbug/lbug-adapter.ts` — fallback per-row `MATCH ... CREATE` insert reads the optional column and threads it into the relationship properties. No language has populated this field yet — the Zig hookup lands in the next commit. Existing DBs need a re-index for the new column to appear; existing readers are unchanged because the field is optional and absent on every other language's edges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit c8cd5efe27a77a5d1b3f05e3f4e89069b5c6b19e) * fix(zig): gate bare literal branches; AND the flag over deduplicated free-call sites PR #3161 review, two findings: 1. `stampZigStaticGating` returned early when the file declared no boolean constants, but `collectZigStaticGatedRanges` also folds bare literals, so `if (false) { foo(); }` in a constant-free file went unstamped. The early return is gone; the range walk runs for every file. 2. `emitFreeCallFallback` deduplicates CALLS edges per (caller, callee) and wrote `staticGated` from whichever site it met first, so a callee reached from one live site and one dead site was gated or not by traversal order. Emission is now deferred to the end of each file's sites and the flag is the AND over every site that collapsed into the edge: one live site keeps the edge live. The other emit path keys its dedup on the site range and was not affected; `collapseByCallerTarget` in the generic bridge would have the same shape but no language that sets the marker opts into it. Fixture + tests: `gated_bare_literal`, `live_and_gated_same_callee` (live site first) and `gated_then_live_same_callee` (dead site first) in zig-static-gating.test.ts. All 70 resolver suites (3,603 tests) pass with the shared emitter change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * test(zig): move the SCHEMA_BUMP pin to 93; rebaseline emit fingerprints for the staticGated column Three CI failures onf4954963, all consequences of this PR: - test/unit/incremental-parse-cache.test.ts pins SCHEMA_BUMP so concurrent bumps cannot collide; 92 -> 93 for #3161 (parse-time call captures gain `@reference.static-gated`), 92 added to the taken list. - bench/emit-persistence `measure.mjs --check` and `measure-streaming.mjs --check`: byte-identity fingerprints drift because every relationship row now ends in a `staticGated` cell. Regenerated with the inverse-operation evidence recorded under `_rebaselined_3161_static_gated_column` in both baseline files: stripping ONLY the new column from the emitted CSVs reproduces the prior fingerprints exactly (36 files, 3 rel_* files differ, 33 byte-identical; 36,000 PDG rows each +2 bytes), so no row moved between pair files or reordered. Timing and retention gates passed throughout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * docs(graph): state the CALLS contract on staticGated; say "provably unreachable at compile time" Review on #3161 (magyargergo): the flag must not redefine what a CALLS edge means. The field's doc now says so explicitly: CALLS still means "there is a resolved call site from A to B", never "B is reachable from A"; `staticGated` is additional, statically provable path-feasibility metadata, an opt-in analysis layer that no core pass acts on. The edge is emitted, persisted, traversed and counted exactly as before. Wording: "unreachable in production" -> "provably unreachable from the indexed source at compile time" on GraphRelationship, ReferenceSite and Reference. The index has no production build configuration and should not claim one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * feat(zig): surface staticGated on impact byDepth items; gate if-expressions and negated/parenthesized conditions Addresses the tri-review on #3161. - impact: the depth traversal already selected r.staticGated but dropped it when building the byDepth item. Forward it (present only when true) and document the field on the impact tool's byDepth contract. Traversal and ranking still do not act on it; that stays opt-in for consumers. - zig-static-gating: walk `if_expression` (`const x = if (c) a() else b();`) in addition to `if_statement`. The expression form has no field names and no else_clause wrapper, so the arms are located positionally (`ifExpressionArms`). Labeled-block arms are covered. - evalCond: `parenthesized_expression` is transparent, so `!(A and B)` and `((FLAG))` fold. Prefix `!` has no unary node in tree-sitter-zig; the header now says exactly which shapes fold instead of "simple negation". - fixture + tests: nine new cases (negation x2, parentheses x2, if-expression then/else/labeled-block x5). Cross-file `@import` constants remain skipped and now cite the tracking issue #3162. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * refactor(zig): drop the unreachable cross-file gating builders; document the real wiring gitnexus-check onb77cb4ed: `buildZigImportAliasMap` / `buildZigRawImportAliasMap` and the per-call ancestor walk (`isCallStaticGated`, `ifBranchDirection`, `nodesEqual`) had no caller anywhere. They were ported from the legacy call-processor design; the scope-resolution provider stamps ranges via `collectZigStaticGatedRanges` instead, and nothing populates the cross-file seam yet (#3162). Remove them so the module exports only what runs. The evaluator keeps `importAliases` + `lookupBoolsForPath` (the seam #3162 will fill); the header now says so explicitly and points at the actual wire-up (`stampZigStaticGating` in languages/zig/captures.ts) instead of the retired `configs/zig.ts` hook. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ciQ3MTjpQXkCoNntZR9zG * refactor(zig): reuse descendantsOfType and tighten static-gated capture matching Walk if-nodes through tree-sitter instead of a hand-rolled stack, return the original capture array when nothing is gated, and register the marker as a known sub-tag so it cannot be mistaken for an anchor. Co-authored-by: Cursor <cursoragent@cursor.com> * style(zig): wrap a long else-clause assignment to satisfy prettier Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.9 KiB
TypeScript
201 lines
6.9 KiB
TypeScript
/**
|
|
* Zig: static-gated CALLS edges.
|
|
*
|
|
* Verifies that calls inside `if (CONST_FALSE)` branches (and trivial
|
|
* boolean-and / boolean-or extensions) get tagged with
|
|
* `staticGated: true` on the emitted CALLS edge, while calls outside
|
|
* such branches keep `staticGated` falsy.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import path from 'path';
|
|
import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js';
|
|
|
|
describe('Zig static-gated edges', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-static-gating'), () => {});
|
|
}, 60000);
|
|
|
|
function isGated(callee: string): boolean | undefined {
|
|
const calls = getRelationships(result, 'CALLS').filter((e) => e.target === callee);
|
|
if (calls.length === 0) return undefined;
|
|
// If any caller-edge to `callee` carries staticGated, treat as gated.
|
|
return calls.some((c) => c.rel.staticGated === true);
|
|
}
|
|
|
|
it('tags calls inside `if (UPGRADERS_ENABLED)` as staticGated', () => {
|
|
expect(isGated('gated_simple')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (FALSE and other)` as staticGated (and-left)', () => {
|
|
expect(isGated('gated_and_left')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (other and FALSE)` as staticGated (and-right)', () => {
|
|
expect(isGated('gated_and_right')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (FALSE or FALSE)` as staticGated', () => {
|
|
expect(isGated('gated_or_both_false')).toBe(true);
|
|
});
|
|
|
|
it('tags a bare `if (false)` gate even when the file declares no bool constants', () => {
|
|
expect(isGated('gated_bare_literal')).toBe(true);
|
|
});
|
|
|
|
it('keeps a deduplicated edge LIVE when a live site precedes a gated site', () => {
|
|
expect(isGated('live_and_gated_same_callee')).toBe(false);
|
|
});
|
|
|
|
it('keeps a deduplicated edge LIVE when a gated site precedes a live site', () => {
|
|
expect(isGated('gated_then_live_same_callee')).toBe(false);
|
|
});
|
|
|
|
it('tags `if (!TRUE_CONST)` (negation of a true constant is dead)', () => {
|
|
expect(isGated('gated_not_true')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag `if (!FALSE_CONST)` (negation of a false constant is live)', () => {
|
|
expect(isGated('live_not_false')).toBe(false);
|
|
});
|
|
|
|
it('tags `if (!(TRUE and TRUE))` (negated parenthesized compound)', () => {
|
|
expect(isGated('gated_not_paren_and')).toBe(true);
|
|
});
|
|
|
|
it('tags `if ((FALSE_CONST))` (parentheses are transparent)', () => {
|
|
expect(isGated('gated_paren_ident')).toBe(true);
|
|
});
|
|
|
|
it('tags the THEN arm of an if-EXPRESSION `x = if (FALSE) a() else b()`', () => {
|
|
expect(isGated('gated_expr_then')).toBe(true);
|
|
expect(isGated('live_expr_else')).toBe(false);
|
|
});
|
|
|
|
it('tags the ELSE arm of an if-EXPRESSION `x = if (TRUE) a() else b()`', () => {
|
|
expect(isGated('live_expr_then')).toBe(false);
|
|
expect(isGated('gated_expr_else')).toBe(true);
|
|
});
|
|
|
|
it('tags calls inside a labeled-block THEN arm of an if-expression', () => {
|
|
expect(isGated('gated_expr_block')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag unconditional calls', () => {
|
|
expect(isGated('live_unconditional')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag calls under `if (TRUE_CONST)`', () => {
|
|
expect(isGated('live_under_true_const')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag `if (FALSE or TRUE)` (disjunction is true)', () => {
|
|
expect(isGated('live_or_one_true')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag calls under unknown / runtime conditions', () => {
|
|
expect(isGated('live_under_unknown')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag calls under `if (var FOO = false)` (var is mutable global, not const)', () => {
|
|
expect(isGated('live_under_var')).toBe(false);
|
|
});
|
|
|
|
it('tags `if (FOO == true)` when FOO is false', () => {
|
|
expect(isGated('gated_eq_true')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (FOO == false)` when FOO is true', () => {
|
|
expect(isGated('gated_eq_false')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (FOO != false)` when FOO is false', () => {
|
|
expect(isGated('gated_neq_false')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (FOO != true)` when FOO is true', () => {
|
|
expect(isGated('gated_neq_true')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag `if (false == FOO)` when FOO is false (provably TRUE)', () => {
|
|
expect(isGated('live_sym_eq')).toBe(false);
|
|
});
|
|
|
|
it('tags re-aliased const chain (1 hop)', () => {
|
|
expect(isGated('gated_alias_one')).toBe(true);
|
|
});
|
|
|
|
it('tags re-aliased const chain (2 hops)', () => {
|
|
expect(isGated('gated_alias_two')).toBe(true);
|
|
});
|
|
|
|
it('tags re-aliased const chain (3 hops)', () => {
|
|
expect(isGated('gated_alias_three')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag alias chain that exits to unknown identifier', () => {
|
|
expect(isGated('live_alias_to_unknown')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag (and does not infinite-loop on) alias cycles', () => {
|
|
expect(isGated('live_alias_cycle')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag alias whose chain root is a `var` (not a const)', () => {
|
|
expect(isGated('live_alias_to_var')).toBe(false);
|
|
});
|
|
|
|
it('tags `if (FALSE) { dead }` THEN branch as gated', () => {
|
|
expect(isGated('gated_then_branch')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag the ELSE branch of `if (FALSE)` (it is live)', () => {
|
|
expect(isGated('live_else_branch')).toBe(false);
|
|
});
|
|
|
|
it('does NOT tag the THEN branch of `if (TRUE)` (it is live)', () => {
|
|
expect(isGated('live_then_branch')).toBe(false);
|
|
});
|
|
|
|
it('tags the ELSE branch of `if (TRUE)` as gated', () => {
|
|
expect(isGated('gated_else_branch')).toBe(true);
|
|
});
|
|
|
|
it('tags `if (FALSE) { dead }` in else-if chain as gated', () => {
|
|
expect(isGated('gated_outer_then')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag the live arm of an else-if chain', () => {
|
|
expect(isGated('live_chain_mid')).toBe(false);
|
|
});
|
|
|
|
it('tags the trailing else of `else if (TRUE)` as gated', () => {
|
|
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)', () => {
|
|
expect(isGated('gated_cross_file_foo')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag the THEN branch of `if (cfg.BAR)` when BAR is true', () => {
|
|
expect(isGated('live_cross_file_bar')).toBe(false);
|
|
});
|
|
|
|
it.skip('tags the ELSE branch of `if (cfg.BAR)` when BAR is true (tracked: #3162)', () => {
|
|
expect(isGated('gated_cross_file_else')).toBe(true);
|
|
});
|
|
|
|
it('does NOT tag `cfg.UNDEFINED_NAME` (member not found in imported file)', () => {
|
|
expect(isGated('live_cross_file_undefined')).toBe(false);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|