mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
The `gitnexus-check` pass on `bd6e577e` carried three findings of its own, and I
answered the later `1c7c05ff` pass without noticing them. Recording that as a
process failure too: bot reviews are per-head, and a later pass does not
necessarily repeat an earlier one's findings.
R6-1 (Error, valid, REPRODUCED) — the shadow guard stopped one rung short.
`isOwnerNameShadowedBySomethingElse` returned `false` on reaching the module
scope, justified as "a container declared there IS the binding, and the caller
already resolved it". That holds when the owner came from the scope chain and
fails when it came from the workspace-wide qualified-name fallback:
// Gauge.zig — never imported by Element.zig
const Gauge = @This();
pub fn read(self: *Gauge) u8 { … }
// Element.zig
const Gauge = @import("dom_utils.zig").DEFAULT_NS; // NOT a container
pub const level = bridge.accessor(Gauge.read, null, .{}); // → Gauge.zig's read
`findClassBindingInScope` steps over the module-scope binding because it is not
class-like, answers from `scopes.qualifiedNames`, and the guard waved it through.
Worth recording: the first fixture attempt did NOT reproduce. A local
`const Gauge: u8 = 3;` also claims the workspace qualified name `Gauge`, leaving
two candidates, and the fallback refuses to guess between two — so the shape
defeated itself. Binding the name by IMPORT claims no qualified name, the
fallback stays unique, and it fires. A negative result on the first shape was not
evidence the finding was wrong.
Fixed by inspecting the module scope as the last rung instead of skipping it.
The identity exemption is what makes that safe where `isNamespaceNameShadowed`
cannot do it (#2723: a namespace import writes its own name into the module scope
and would read as its own shadow) — the binding that IS the owner exempts itself,
and only a binding to something else answers `true`. `lookupBindingsAt` is
consulted at that scope and only there, because an imported alias lives in the
finalized channel rather than in `scope.bindings`.
R6-2 — the hub re-export finding, already fixed in `5d8fe9d8`; the same defect
restated on the later head.
R6-3 (valid, fixed) — the dispatchability canary omitted the TSX suffix.
`getTsScopeQuery` analyzes a `.tsx` file with `TYPESCRIPT_SCOPE_QUERY +
TSX_JSX_QUERY_SUFFIX`, and the test read only the base, so a `value-ref` rule
added to the suffix would be emitted in TSX analysis with the canary green. The
suffix is now exported and concatenated into the check; verified load-bearing by
adding an unkeyed `jsx_expression` value-ref rule to it, which fails the
TypeScript case.
Gates: tsc --noEmit clean, npm run build clean, prettier clean.
`test/integration/resolvers` 3,635 passed / 3 skipped (70 files);
`test/unit/scope-resolution` 2,015 passed (120 files);
`impact-callable-value-references` 7 passed under `lbug-db`. Bench --check:
receiver-resolution, zig-cross-file-resolution, scope-capture (15 languages),
scope-emission, callable-value-flow all PASS with no baseline edited.
173 lines
7.2 KiB
TypeScript
173 lines
7.2 KiB
TypeScript
/**
|
|
* Canary for the invariant that `callableValueReferenceBoundaries`' dispatch
|
|
* exclusion silently depends on (#3219 review round 3).
|
|
*
|
|
* The exclusion, in `mcp/local/local-backend.ts`: a target with an inbound
|
|
* `property-dispatch` CALLS edge is NOT hedged, because the analyzer followed
|
|
* the registration and nothing was missed. It is symbol-level, not edge-level —
|
|
* the graph does not record which registration produced which synthesized call.
|
|
*
|
|
* That is only sound while no single symbol can carry BOTH kinds of
|
|
* registration, and today none can, for a reason that lives nowhere near the
|
|
* exclusion:
|
|
*
|
|
* - `emitPropertyDispatchCalls` synthesizes a CALLS edge only for a
|
|
* registration whose site carries a `propertyKey` (sweep 1 skips the
|
|
* registration index when it is undefined; sweep 2 reads only that index).
|
|
* - Every JS/TS `@reference.value-ref` rule also captures
|
|
* `@reference.property-key` — both are object-literal shapes.
|
|
* - No Zig `@reference.value-ref` rule captures one: Zig has no
|
|
* object-literal key to dispatch through.
|
|
*
|
|
* So a dispatchable registration is always a JS/TS one and an undispatchable
|
|
* registration is always a Zig one, and the two cannot meet on one symbol.
|
|
*
|
|
* The day that stops being true — a JS/TS rule for a bare callback argument
|
|
* (`register(handler)`), a Zig rule that grows a key — a symbol CAN have both,
|
|
* and the exclusion starts publishing `exact` over a registration the analyzer
|
|
* provably did not follow. That is the #3399 defect returning through a side
|
|
* door, and it would not fail a single existing test.
|
|
*
|
|
* This test fails instead. If it fails, do not relax it: go and decide what
|
|
* `callableValueReferenceBoundaries` should do about a mixed symbol (the
|
|
* options are recorded at the exclusion site), then update this file.
|
|
*
|
|
* WHAT IT DOES NOT COVER, stated so the green tick is not read as more than it
|
|
* is. It reads query SOURCES, so a capture synthesized in code rather than
|
|
* matched by a rule — the mechanism `@reference.static-gated` uses — can break
|
|
* the partition with this test green. A provider adding one has to come here by
|
|
* hand. Languages that own no query and delegate to another's captures (Vue →
|
|
* `emitTsScopeCaptures` / `emitJsScopeCaptures`) are covered transitively, by
|
|
* the rules they borrow, which is why the last case asserts on query OWNERS
|
|
* rather than on the set of languages that can emit a value-ref.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
TSX_JSX_QUERY_SUFFIX,
|
|
TYPESCRIPT_SCOPE_QUERY,
|
|
} from '../../../src/core/ingestion/languages/typescript/query.js';
|
|
import { JAVASCRIPT_SCOPE_QUERY } from '../../../src/core/ingestion/languages/javascript/query.js';
|
|
import { ZIG_SCOPE_QUERY } from '../../../src/core/ingestion/languages/zig/query.js';
|
|
|
|
const VALUE_REF = '@reference.value-ref';
|
|
const PROPERTY_KEY = '@reference.property-key';
|
|
|
|
/**
|
|
* Split a tree-sitter scope query into its top-level s-expression rules.
|
|
*
|
|
* `;;` comments are dropped first — they discuss the very tags this test
|
|
* matches on (Zig's rules carry a paragraph explaining why they attach no
|
|
* property key), so leaving them in would make every Zig rule look keyed.
|
|
* Double-quoted anonymous nodes (`"const"`, `"("`) are skipped while counting
|
|
* depth: a query that matches a literal paren would otherwise unbalance it.
|
|
*/
|
|
function topLevelRules(query: string): string[] {
|
|
const src = query
|
|
.split('\n')
|
|
.map((line) => {
|
|
const comment = line.indexOf(';;');
|
|
return comment === -1 ? line : line.slice(0, comment);
|
|
})
|
|
.join('\n');
|
|
|
|
const rules: string[] = [];
|
|
let depth = 0;
|
|
let start = -1;
|
|
let inString = false;
|
|
for (let i = 0; i < src.length; i++) {
|
|
const ch = src[i];
|
|
if (inString) {
|
|
if (ch === '\\') i++;
|
|
else if (ch === '"') inString = false;
|
|
continue;
|
|
}
|
|
if (ch === '"') {
|
|
inString = true;
|
|
continue;
|
|
}
|
|
if (ch === '(') {
|
|
if (depth === 0) start = i;
|
|
depth++;
|
|
} else if (ch === ')') {
|
|
depth--;
|
|
if (depth === 0 && start !== -1) {
|
|
rules.push(src.slice(start, i + 1));
|
|
start = -1;
|
|
}
|
|
if (depth < 0) depth = 0;
|
|
}
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
function valueRefRules(query: string): string[] {
|
|
return topLevelRules(query).filter((rule) => rule.includes(VALUE_REF));
|
|
}
|
|
|
|
describe('value-ref dispatchability partition', () => {
|
|
it('splits a query into rules without being confused by comments or literal parens', () => {
|
|
// Guards the guard: a splitter that silently returned [] would make every
|
|
// assertion below vacuously true.
|
|
const rules = topLevelRules(`
|
|
;; a comment mentioning (parens) and ${PROPERTY_KEY}
|
|
(call_expression
|
|
function: (_)
|
|
(identifier) @reference.name)
|
|
|
|
(variable_declaration
|
|
"const" . (identifier) @a .)
|
|
`);
|
|
expect(rules).toHaveLength(2);
|
|
expect(rules[0]).toContain('call_expression');
|
|
expect(rules[1]).toContain('variable_declaration');
|
|
expect(rules.join('\n')).not.toContain(PROPERTY_KEY);
|
|
});
|
|
|
|
it('every TypeScript value-ref rule is DISPATCHABLE (carries a property key)', () => {
|
|
// The BASE query plus the TSX suffix, because `getTsScopeQuery` concatenates
|
|
// them for a `.tsx` file: a value-ref rule added to the suffix alone would
|
|
// be emitted in TSX analysis while a base-only check stayed green.
|
|
const rules = valueRefRules(TYPESCRIPT_SCOPE_QUERY + TSX_JSX_QUERY_SUFFIX);
|
|
expect(rules.length).toBeGreaterThan(0);
|
|
expect(rules.filter((r) => !r.includes(PROPERTY_KEY))).toEqual([]);
|
|
});
|
|
|
|
it('every JavaScript value-ref rule is DISPATCHABLE (carries a property key)', () => {
|
|
const rules = valueRefRules(JAVASCRIPT_SCOPE_QUERY);
|
|
expect(rules.length).toBeGreaterThan(0);
|
|
expect(rules.filter((r) => !r.includes(PROPERTY_KEY))).toEqual([]);
|
|
});
|
|
|
|
it('every Zig value-ref rule is UNDISPATCHABLE (carries no property key)', () => {
|
|
const rules = valueRefRules(ZIG_SCOPE_QUERY);
|
|
expect(rules.length).toBeGreaterThan(0);
|
|
expect(rules.filter((r) => r.includes(PROPERTY_KEY))).toEqual([]);
|
|
});
|
|
|
|
it('no OTHER language OWNS a value-ref rule', () => {
|
|
// The three above are hand-classified. A fourth query declaring
|
|
// `value-ref` has not been classified by anyone, so the exclusion's premise
|
|
// is unverified for it — classify it here and in the exclusion's comment.
|
|
// "Owns", not "emits": Vue has no query of its own and borrows TypeScript's
|
|
// and JavaScript's captures, so it inherits their classification rather than
|
|
// needing one. A capture synthesized in code owns no rule either and is
|
|
// invisible here — see the header.
|
|
const languagesDir = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../../../src/core/ingestion/languages',
|
|
);
|
|
const emitting = fs
|
|
.readdirSync(languagesDir, { withFileTypes: true })
|
|
.filter((e) => e.isDirectory())
|
|
.filter((e) => {
|
|
const query = path.join(languagesDir, e.name, 'query.ts');
|
|
return fs.existsSync(query) && fs.readFileSync(query, 'utf8').includes(VALUE_REF);
|
|
})
|
|
.map((e) => e.name)
|
|
.sort();
|
|
expect(emitting).toEqual(['javascript', 'typescript', 'zig']);
|
|
});
|
|
});
|