mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
Found by running the local preflight harness before pushing rather than after. One of its five findings is a real hole in R2-2; the rest are documentation that overclaimed. R4-1 (valid, REPRODUCED) — the container channel preempted the file's own `@import`. R2-2 added the module channel as a FALLBACK after `findClassBindingInScope`, and that order is wrong: `findClassBindingInScope` does not stop at the scope chain. When its `isClassLike` walk misses — and a namespace handle binds a Module, so it always misses — it falls back to `scopes.qualifiedNames`, a workspace-wide index, and answers with the unique def of that name anywhere in the repo. A container named `dom_utils` in a file `Element.zig` never imports therefore captured `bridge.accessor(dom_utils.compare, …)`, binding `Method:src/webapi/decoy.zig:dom_utils.compare#2` while the module channel that would have answered correctly was never reached. R3-1's shadow guard cannot catch it: the import binds at MODULE scope, which that guard treats as the floor. Fixed by trying the module channel FIRST. An `@import` written in this file is the strongest available statement about what the name means here and outranks a global uniqueness guess; when the handle is not an import of this file the channel answers nothing and the container path runs exactly as before. Pinned by `decoy.zig` and a strengthened assertion on the existing module-owner test, which fails on the old order. R4-2 — the cause documentation named shapes nothing captures. `tools.ts` illustrated `callableValueReferences` with "a callback argument", "a stored function pointer" and `qsort(xs, n, sz, compareItems)`. Only Zig captures a call argument or a const initialiser; JS/TS capture only object-literal property values, and C has no value-ref rule, so the `qsort` example is counted in no language. Both cause blocks now name the captured shapes and say that a bare JS/TS callback argument is not among them, so a 0 does not rule it out. R4-3 — the same block exempted itself from the re-index caveat this PR proves it needs. "Read from the graph, so it needs no index-time metadata" is true of the probe and false of the edges: an index built before a language emitted these captures has none and reports 0 — the warm-cache failure R2-1 bumped SCHEMA_BUMP for. It now says to re-analyze before reading a 0 as measured. R4-4 — the dispatchability canary was narrower than its own promise. Its header claimed it fails on "a fourth language emitting value-ref at all"; it reads `languages/<dir>/query.ts`, so Vue — which owns no query and borrows `emitTsScopeCaptures` / `emitJsScopeCaptures` — emits value-refs while the assertion lists three languages, and a capture synthesized in code is invisible to it. The case now asserts on query OWNERS, which is what it checks and is sound because a delegating language inherits the rules it borrows; the header states the synthesized-capture gap instead of letting a green tick imply it away. R4-5 — the BARE docstring described a lexical walk that is not one. `findCallableBindingInScope` applies the callable predicate WHILE walking, so a nearer parameter or local is stepped over: the defect R3-1 fixed on the container channel, unguarded here, pre-existing since #2437 and reachable in JS/TS. Out of scope for #3399, so behaviour is unchanged and the sentence now says what the walk does rather than implying a guarantee it does not give. Gates: tsc --noEmit clean, npm run build clean, prettier clean. `test/integration/resolvers` 3,632 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.
167 lines
6.9 KiB
TypeScript
167 lines
6.9 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 { 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)', () => {
|
|
const rules = valueRefRules(TYPESCRIPT_SCOPE_QUERY);
|
|
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']);
|
|
});
|
|
});
|