Merge branch 'main' into main

This commit is contained in:
Gergő Magyar 2026-05-30 22:19:48 +01:00 • committed by GitHub
commit 838c164155
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 5741 additions and 163 deletions

View file

@ -12,7 +12,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
# persist-credentials: false — this job runs tests and uploads a
# test-reports artifact (if: always()). The default-persisted token in
# .git/config must not be capturable through that upload (zizmor
# credential-persistence / artipacked audit). The job never pushes.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
@ -72,7 +78,11 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
# persist-credentials: false — runs tests only, never pushes (zizmor
# credential-persistence / artipacked audit).
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
@ -181,3 +191,60 @@ jobs:
else
"$PREFIX/bin/gitnexus" --version
fi
# ── Dedicated benchmark gate ─────────────────────────────────────
# The cross-language `*-pipeline-benchmark.test.ts` suites are gated behind
# GITNEXUS_BENCH (they generate synthetic codebases at scale), so the main
# coverage job above SKIPS them — their O(n^2) scaling guards never ran in CI.
# Run them here with GITNEXUS_BENCH=1, alongside the Python scope-capture and
# import-resolution fingerprint + scaling guards (PR #1918 P2a).
#
# `--no-file-parallelism` is REQUIRED: these suites measure wall-clock and peak
# heap, so parallel forks both skew the timings and OOM the worker pool — they
# must run one file at a time.
#
# go-pipeline-benchmark.test.ts is deliberately NOT included: its
# worker-pool (#1848) suite spins a real worker pool that exits unexpectedly
# under vitest's fork pool (reproduced in validation), which would make this
# gate flaky. Go is already guarded by its non-gated O(n^2) tripwire (runs in
# the main coverage job) plus its golden capture-parity test.
benchmarks:
name: benchmarks (GITNEXUS_BENCH)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
# persist-credentials: false — this job only runs npm + vitest benchmarks
# and never pushes; the default-persisted token in .git/config would be at
# risk of leaking through an artifact upload (zizmor credential-persistence
# / artipacked audit). Mirrors the packaged-install-smoke job below.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
- name: Python scope-capture + import-resolution fingerprint / scaling guards
run: |
node --import tsx bench/python-scope/measure.mjs --check
node --import tsx bench/python-scope/import-target-fingerprint.mjs --check
working-directory: gitnexus
- name: Cross-language scope-capture fingerprint + scaling guards
# Build-free: asserts emit<Lang>ScopeCaptures output is unchanged
# (fingerprint) and stays linear (scaling < 1.5) for go/csharp/rust/php/
# ruby/cobol. Catches an O(n^2) re-regression without the worker pool.
run: node --import tsx bench/scope-capture/measure.mjs --check
working-directory: gitnexus
- name: Cross-language pipeline benchmarks (GITNEXUS_BENCH, serial)
env:
GITNEXUS_BENCH: '1'
run: >-
npx vitest run --no-file-parallelism
test/integration/cobol-pipeline-benchmark.test.ts
test/integration/csharp-pipeline-benchmark.test.ts
test/integration/rust-pipeline-benchmark.test.ts
test/integration/php-pipeline-benchmark.test.ts
test/integration/ruby-pipeline-benchmark.test.ts
working-directory: gitnexus

View file

@ -0,0 +1 @@
f2b4376f30dab76f3befc9cbd3d7cc2bf1afbd7329a5e953439083e005de4a7c

View file

@ -0,0 +1 @@
d51ea9edd1902fc20dd888f3fc51907c8119a4692b92c32a427801ac7f41d451

View file

@ -0,0 +1,202 @@
/**
* Resolver-output correctness fingerprint for `resolvePythonImportTarget`
* (ce-optimize: python-scope-capture, hypothesis H2).
*
* H2 replaces the per-import O(files) suffix scan + candidate scan in
* import-target.ts with a memoized index. The index MUST reproduce the exact
* resolution result (including the deterministic tie-break and the
* false-positive gating) for every input. This harness pins that: it runs
* resolvePythonImportTarget over an exhaustive branch matrix PLUS a large
* deterministic fuzz (varied repo layouts that force collisions / multi-match
* tie-breaks), and prints an order-independent sha256 over every
* `fromFile | targetRaw | result` triple.
*
* Build-free via tsx (static .ts import). Run:
* node --import tsx bench/python-scope/import-target-fingerprint.mjs
*
* The fingerprint before and after the H2 change MUST be identical.
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolvePythonImportTarget } from '../../src/core/ingestion/languages/python/import-target.ts';
function mkImport(targetRaw) {
return { kind: 'absolute', targetRaw, isRelative: false, names: [] };
}
function resolve(fromFile, files, targetRaw) {
const ctx = { fromFile, allFilePaths: new Set(files) };
return resolvePythonImportTarget(mkImport(targetRaw), ctx);
}
const lines = [];
let nonNull = 0;
function record(fromFile, files, targetRaw) {
const r = resolve(fromFile, files, targetRaw);
if (r !== null) nonNull++;
lines.push(`${fromFile}\t${targetRaw}\t${r === null ? 'NULL' : r}`);
}
// ---- 1. Exhaustive branch matrix ----------------------------------------
// direct root hit
record('app/main.py', ['services/sync.py', 'services/__init__.py'], 'services.sync');
// direct package (__init__) hit
record('app/main.py', ['services/__init__.py'], 'services');
// ancestor walk hit
record('backend/routers/cron.py', ['backend/services/sync.py'], 'services.sync');
// ancestor pkg hit
record('backend/routers/cron.py', ['backend/services/__init__.py'], 'services');
// suffix fallback single match (nested vendor layout)
record('app/main.py', ['pkg/__init__.py', 'vendor/pkg/thing.py'], 'pkg.thing');
// suffix fallback to __init__ (package)
record('app/main.py', ['pkg/__init__.py', 'x/pkg/subpkg/__init__.py'], 'pkg.subpkg');
// suffix multi-match tie-break: fewest segments wins
record('app/main.py', ['pkg/__init__.py', 'a/pkg/models.py', 'b/c/pkg/models.py'], 'pkg.models');
// suffix multi-match tie-break at SAME depth: lexicographic
record('app/main.py', ['pkg/__init__.py', 'z/pkg/models.py', 'a/pkg/models.py'], 'pkg.models');
// suffix file vs pkg same name, mixed — both candidate forms present
record(
'app/main.py',
['pkg/__init__.py', 'q/pkg/models.py', 'r/pkg/models/__init__.py'],
'pkg.models',
);
// hasRepoCandidate FALSE — external dotted import w/ colliding local basename (django.apps guard)
record('app/main.py', ['accounts/apps.py'], 'django.apps');
// hasRepoCandidate TRUE via top-level package, but no concrete file -> null
record('app/main.py', ['pkg/__init__.py'], 'pkg.ghost');
// hasRepoCandidate via nested ancestor namespace package
record('backend/routers/cron.py', ['backend/services/sync.py'], 'services.helpers.util');
// collision: accounts.models must NOT match billing/models.py
record('app/main.py', ['accounts/__init__.py', 'billing/models.py'], 'accounts.models');
// relative imports
record('app/main.py', ['app/sibling.py'], '.sibling');
record('app/pkg/mod.py', ['app/sibling.py'], '..sibling');
record('app/main.py', ['app/sibling.py'], '...way.too.far');
// single-segment bare import (no '/'): skips candidate gate
record('app/main.py', ['mod.py'], 'mod');
record('app/main.py', ['lib/mod.py'], 'mod');
record('app/pkg/main.py', ['app/pkg/local.py'], 'local');
// empty / dynamic
record('app/main.py', ['a.py'], '');
// windows-style backslash paths in the set
record('app\\main.py', ['svc\\sync.py', 'svc\\__init__.py'], 'svc.sync');
// ---- 2. Deterministic fuzz ----------------------------------------------
// LCG (no Math.random — deterministic + reproducible).
let seed = 0x9e3779b9;
function rnd() {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 0x100000000;
}
function pick(arr) {
return arr[Math.floor(rnd() * arr.length)];
}
const DIRS = ['', 'a', 'b', 'a/b', 'b/c', 'x/y/z', 'vendor', 'src', 'src/app', 'pkg'];
const SEGS = [
'pkg',
'services',
'models',
'sync',
'util',
'core',
'apps',
'sub',
'thing',
'helpers',
];
function randPath() {
const dir = pick(DIRS);
const base = pick(SEGS);
const isPkg = rnd() < 0.3;
const file = isPkg ? `${base}/__init__.py` : `${base}.py`;
return dir ? `${dir}/${file}` : file;
}
function randDotted() {
const n = 1 + Math.floor(rnd() * 3);
const parts = [];
for (let i = 0; i < n; i++) parts.push(pick(SEGS));
const rel = rnd() < 0.15 ? '.'.repeat(1 + Math.floor(rnd() * 2)) : '';
return rel + parts.join('.');
}
for (let repo = 0; repo < 400; repo++) {
const fileCount = 3 + Math.floor(rnd() * 14);
const files = [];
for (let i = 0; i < fileCount; i++) files.push(randPath());
const fromFile = randPath();
for (let imp = 0; imp < 25; imp++) {
record(fromFile, files, randDotted());
}
}
// ---- 3. Absolute-path coverage (PR #1918 review P3a) --------------------
// Production paths are repo-relative, but the index's prefix gating must
// reproduce the old `f.startsWith(prefix)` semantics for absolute paths too.
// The reviewer's exact case + a fuzz over leading-`/` file sets and absolute
// importer paths lock the absolute-path behavior end to end.
// The flagged case: an absolute file under the importer's own root.
record('/repo/app/main.py', ['/repo/svc/x.py'], 'svc.x');
record('/repo/app/main.py', ['/repo/svc/__init__.py', '/repo/svc/x.py'], 'svc.x');
// Absolute file NOT under the importer root — gate must not pass it.
record('/repo/app/main.py', ['/other/svc/x.py'], 'svc.x');
// Absolute vendored layout reachable only by suffix.
record('/repo/app/main.py', ['/repo/pkg/__init__.py', '/repo/vendor/pkg/thing.py'], 'pkg.thing');
// Absolute tie-break.
record(
'/repo/app/main.py',
['/repo/pkg/__init__.py', '/a/pkg/models.py', '/b/c/pkg/models.py'],
'pkg.models',
);
// Mixed absolute/relative file set.
record('/repo/app/main.py', ['/repo/pkg/__init__.py', 'pkg/models.py'], 'pkg.models');
function randAbsPath() {
// Reuse the relative generator under one of a few absolute roots.
const root = pick(['/repo', '/srv/app', '/']);
const rel = randPath();
return root === '/' ? `/${rel}` : `${root}/${rel}`;
}
for (let repo = 0; repo < 200; repo++) {
const fileCount = 3 + Math.floor(rnd() * 12);
const files = [];
for (let i = 0; i < fileCount; i++) files.push(randAbsPath());
const fromFile = randAbsPath();
for (let imp = 0; imp < 20; imp++) {
record(fromFile, files, randDotted());
}
}
const fingerprint = crypto
.createHash('sha256')
.update([...lines].sort().join('\n'))
.digest('hex');
const result = { fingerprint, cases: lines.length, non_null: nonNull };
if (!process.argv.includes('--check')) {
process.stdout.write(JSON.stringify(result) + '\n');
} else {
// CI gate: resolver output unchanged (fingerprint == committed baseline).
// Re-baseline a legitimate resolution change by running without --check and
// committing the new baseline-import-target-fingerprint.txt deliberately.
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const baseline = fs
.readFileSync(path.resolve(__dirname, 'baseline-import-target-fingerprint.txt'), 'utf8')
.trim();
process.stdout.write(JSON.stringify(result) + '\n');
if (result.fingerprint !== baseline) {
process.stderr.write(
`[import-target-fingerprint --check] FAIL: resolver fingerprint drift: got ` +
`${result.fingerprint}, expected ${baseline} (resolvePythonImportTarget output changed — ` +
`re-baseline intentionally if expected)\n`,
);
process.exit(1);
}
process.stderr.write('[import-target-fingerprint --check] PASS (resolver fingerprint)\n');
}

View file

@ -0,0 +1,216 @@
/**
* Build-free measurement harness for `emitPythonScopeCaptures`
* (ce-optimize: python-scope-capture).
*
* This is Python's counterpart to `bench/scope-capture/measure.mjs` (which
* covers go/csharp/rust/php/ruby/cobol). Python lives here, NOT in that unified
* harness, because this one ALSO covers import resolution
* (`import-target-fingerprint.mjs`). Python's capture-scaling guard therefore
* runs via `python-scope/measure.mjs --check`, not the unified harness — don't
* remove either thinking the other covers Python.
*
* Mirrors the Go scope-capture harness (#1848). Imports the `.ts` hotpath
* directly through tsx (`node --import tsx bench/python-scope/measure.mjs`):
* a static `.ts` import works; a top-level `await import()` breaks tsx's lexer.
*
* Emits ONE JSON object on stdout with:
* - elapsed_ms_250 / elapsed_ms_800: median wall-clock (ms) of
* emitPythonScopeCaptures over a synthetic DAO-style source at that many
* top-level entities (warmed up first). 800/250 ~ 3.2x input; an O(n^2)
* path scales ~quadratically, an O(n) path ~linearly.
* - scaling_ratio: (t800/t250)/(800/250). ~3.2 = quadratic, ~1.0 = linear.
* - capture_groups_250 / capture_groups_800: match counts (a fast-but-empty
* regression can't pass — counts must stay > 0).
* - fingerprint: order-independent sha256 over emitPythonScopeCaptures output
* across the whole lang-resolution/python-* fixture corpus + a fixed
* 20-entity synthetic DAO. This is the CORRECTNESS gate: any change to the
* captures changes the fingerprint. Entity-count-fixed so it is comparable
* across experiments regardless of the timing sizes.
* - capture_groups_fp / fixture_count: corpus sanity.
*/
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { emitPythonScopeCaptures } from '../../src/core/ingestion/languages/python/captures.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', 'test', 'fixtures', 'lang-resolution');
// ---- correctness fingerprint (order-independent, mirrors the Go golden) ----
function canonicalizeMatch(match) {
const parts = [];
for (const tag of Object.keys(match)) {
const cap = match[tag];
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
function digestCaptures(matches) {
const matchStrings = matches.map(canonicalizeMatch).sort();
return crypto.createHash('sha256').update(matchStrings.join('\n')).digest('hex');
}
/** All `.py` files under `lang-resolution/python-*`, sorted by repo-relative key. */
function collectPythonFixtures() {
const out = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('python-')) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop();
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (c.name.endsWith('.py')) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
/**
* Synthetic DAO-style source: top-level imports + N classes (each with methods,
* exercising @scope.function + @declaration.function + receiver binding) + N
* module functions. Maximizes top-level children (rootChildren) AND function
* matches, which is exactly the O(matches x rootChildren) shape #1848 hit.
*/
function generatePyDao(entityCount) {
const lines = [];
for (let i = 0; i < 12; i++) {
lines.push(`from pkg.mod${i} import alpha${i}, beta${i}, gamma${i} as g${i}`);
lines.push(`import top.level.module${i}`);
}
lines.push('');
for (let i = 0; i < entityCount; i++) {
const n = String(i).padStart(4, '0');
lines.push(
`class Entity${n}:`,
` def __init__(self, id: int, name: str):`,
` self.id = id`,
` self.name = name`,
` def get_id(self) -> int:`,
` return self.id`,
` def set_name(self, name: str) -> None:`,
` self.name = name`,
` @classmethod`,
` def make(cls, id: int):`,
` return cls(id, "x")`,
'',
`def build_entity${n}(id: int, name: str) -> Entity${n}:`,
` return Entity${n}(id, name)`,
'',
);
}
return lines.join('\n');
}
// ---- timing ----
function timeOnce(src, filePath) {
const start = process.hrtime.bigint();
const matches = emitPythonScopeCaptures(src, filePath);
const end = process.hrtime.bigint();
return { ms: Number(end - start) / 1e6, count: matches.length };
}
function median(xs) {
const s = [...xs].sort((a, b) => a - b);
const m = Math.floor(s.length / 2);
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
function measureSize(entityCount, reps) {
const src = generatePyDao(entityCount);
// Warm up parser/query JIT (not counted).
timeOnce(src, 'warmup.py');
const samples = [];
let count = 0;
for (let i = 0; i < reps; i++) {
const r = timeOnce(src, `bench-${entityCount}.py`);
samples.push(r.ms);
count = r.count;
}
return { ms: median(samples), count };
}
// ---- run ----
function computeFingerprint() {
let groups = 0;
const perFixtureDigests = [];
for (const { key, absPath } of collectPythonFixtures()) {
const src = fs.readFileSync(absPath, 'utf8');
const matches = emitPythonScopeCaptures(src, absPath);
groups += matches.length;
perFixtureDigests.push(`${key}\t${matches.length}\t${digestCaptures(matches)}`);
}
// Fixed 20-entity synthetic source so the fingerprint is comparable across
// experiments independent of the timing sizes.
const daoMatches = emitPythonScopeCaptures(generatePyDao(20), 'synthetic-dao-20.py');
groups += daoMatches.length;
perFixtureDigests.push(`synthetic:dao-20\t${daoMatches.length}\t${digestCaptures(daoMatches)}`);
const fingerprint = crypto
.createHash('sha256')
.update(perFixtureDigests.sort().join('\n'))
.digest('hex');
return { fingerprint, groups, fixtureCount: perFixtureDigests.length };
}
// Higher rep count keeps the median stable on noisy shared CI runners.
const REPS = 7;
const SCALING_BUDGET = 1.5; // ~3.2 (quadratic) vs ~1.0 (linear); 1.5 has headroom.
const CHECK = process.argv.includes('--check');
const fp = computeFingerprint();
const small = measureSize(250, REPS);
const large = measureSize(800, REPS);
const scalingRatio = small.ms > 0 ? large.ms / small.ms / (800 / 250) : 0;
const result = {
elapsed_ms_250: Number(small.ms.toFixed(2)),
elapsed_ms_800: Number(large.ms.toFixed(2)),
scaling_ratio: Number(scalingRatio.toFixed(3)),
capture_groups_250: small.count,
capture_groups_800: large.count,
fingerprint: fp.fingerprint,
capture_groups_fp: fp.groups,
fixture_count: fp.fixtureCount,
};
if (!CHECK) {
process.stdout.write(JSON.stringify(result) + '\n');
} else {
// CI gate: capture output unchanged (fingerprint == committed baseline) AND
// the path is still linear (scaling ratio under budget). Re-baseline a
// legitimate capture change with `node --import tsx measure.mjs` (no --check)
// and commit the new baseline-fingerprint.txt deliberately.
const baselinePath = path.resolve(__dirname, 'baseline-fingerprint.txt');
const baseline = fs.readFileSync(baselinePath, 'utf8').trim();
const failures = [];
if (result.fingerprint !== baseline) {
failures.push(
`capture fingerprint drift: got ${result.fingerprint}, expected ${baseline} ` +
`(emitPythonScopeCaptures output changed — re-baseline intentionally if expected)`,
);
}
if (result.scaling_ratio >= SCALING_BUDGET) {
failures.push(
`capture scaling ratio ${result.scaling_ratio} >= ${SCALING_BUDGET} ` +
`(possible O(n^2) regression; 250->800ms ${result.elapsed_ms_250}->${result.elapsed_ms_800})`,
);
}
process.stdout.write(JSON.stringify(result) + '\n');
if (failures.length > 0) {
for (const f of failures) process.stderr.write(`[measure --check] FAIL: ${f}\n`);
process.exit(1);
}
process.stderr.write('[measure --check] PASS (capture fingerprint + scaling)\n');
}

View file

@ -0,0 +1,27 @@
{
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. All six languages now thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp this PR).",
"go": {
"fingerprint": "faca3555c61ed6980d2b739bf6b1cac7f4ad4644968a27e4687532d9835cd4c7",
"scaling_budget": 1.5
},
"cobol": {
"fingerprint": "575016f329c0be29eb90db974f750d02a21b4a12515f7029bda312df713b27b0",
"scaling_budget": 1.5
},
"csharp": {
"fingerprint": "bdc7803046011876b2d21ae38e9cb8c97ca1e01769f93ca8affe9317585427bf",
"scaling_budget": 1.5
},
"rust": {
"fingerprint": "025f5b6d4cf1d8cc42033f1f6b592f8d5428e571939c7f61df4d34b4bbe14be3",
"scaling_budget": 1.5
},
"php": {
"fingerprint": "00fe6e83cebd67c5f346fedb4234ebedf192995f9f171a424551cb792a0b91a9",
"scaling_budget": 1.5
},
"ruby": {
"fingerprint": "0f44b0d153b4534866589db93c582928651238b319cb606f2c6396362770cc18",
"scaling_budget": 1.5
}
}

View file

@ -0,0 +1,266 @@
/**
* Unified build-free scope-capture measurement harness for every currently
* benchmarked language (the ones with a `*-pipeline-benchmark.test.ts`):
* go, csharp, rust, php, ruby, cobol — plus python lives in its own
* `bench/python-scope/` harness (richer: it also covers import resolution).
*
* For each language it:
* - times `emit<Lang>ScopeCaptures` on a synthetic DAO-style source at two
* sizes (250 / 800 top-level entities), reporting elapsed_ms + a scaling
* ratio `(t_large/t_small)/(800/250)`: ~1.0 is linear, ~3.2 is quadratic
* (the O(matches × rootChildren) shape #1848 hit in Go);
* - computes an order-independent sha256 fingerprint over the whole
* `lang-resolution/<lang>-*` fixture corpus + a fixed 20-entity synthetic
* source, as the correctness gate.
*
* Build-free: imports the `.ts` hotpaths through tsx
* (`node --import tsx bench/scope-capture/measure.mjs`). Static `.ts` imports
* work; a top-level `await import()` breaks tsx's lexer.
*
* Without args: prints one JSON object per language.
* With `--check`: asserts each language's fingerprint == its committed baseline
* (baselines.json) AND scaling_ratio < that language's recorded budget; exits
* non-zero on any drift/regression.
*/
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { emitGoScopeCaptures } from '../../src/core/ingestion/languages/go/index.ts';
import { emitCsharpScopeCaptures } from '../../src/core/ingestion/languages/csharp/index.ts';
import { emitRustScopeCaptures } from '../../src/core/ingestion/languages/rust/index.ts';
import { emitPhpScopeCaptures } from '../../src/core/ingestion/languages/php/index.ts';
import { emitRubyScopeCaptures } from '../../src/core/ingestion/languages/ruby/index.ts';
import { emitCobolScopeCaptures } from '../../src/core/ingestion/languages/cobol/index.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', 'test', 'fixtures', 'lang-resolution');
const BASELINE_PATH = path.resolve(__dirname, 'baselines.json');
// ---- correctness fingerprint (order-independent; mirrors python harness) ----
function canonicalizeMatch(match) {
const parts = [];
for (const tag of Object.keys(match)) {
const cap = match[tag];
if (cap === undefined || cap === null || cap.range === undefined) {
parts.push(`${tag}|<no-range>`);
continue;
}
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
function digestCaptures(matches) {
return crypto
.createHash('sha256')
.update(matches.map(canonicalizeMatch).sort().join('\n'))
.digest('hex');
}
/** All fixture files for a language, sorted by repo-relative key. */
function collectFixtures(prefix, exts) {
const out = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith(`${prefix}-`)) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop();
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (exts.some((e) => c.name.endsWith(e))) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
// ---- per-language config: synthetic DAO generators + fixture globs ----
const LANGS = [
{
name: 'go',
emit: emitGoScopeCaptures,
fixturePrefix: 'go',
exts: ['.go'],
file: 'bench.go',
header: 'package generated\n\n',
unit: (n) =>
`type Entity${n} struct {\n\tid int64\n\tname string\n}\n\n` +
`func (e *Entity${n}) GetID() int64 { return e.id }\n` +
`func (e *Entity${n}) SetName(v string) { e.name = v }\n\n`,
},
{
name: 'csharp',
emit: emitCsharpScopeCaptures,
fixturePrefix: 'csharp',
exts: ['.cs'],
file: 'bench.cs',
header: 'namespace Generated;\n\n',
unit: (n) =>
`public class Entity${n} {\n` +
` public long Id;\n public string Name;\n` +
` public long GetId() { return Id; }\n` +
` public void SetName(string v) { Name = v; }\n}\n\n`,
},
{
name: 'rust',
emit: emitRustScopeCaptures,
fixturePrefix: 'rust',
exts: ['.rs'],
file: 'bench.rs',
header: '',
unit: (n) =>
`struct Entity${n} {\n id: i64,\n name: String,\n}\n\n` +
`impl Entity${n} {\n` +
` fn get_id(&self) -> i64 { self.id }\n` +
` fn set_name(&mut self, v: String) { self.name = v; }\n}\n\n`,
},
{
name: 'php',
emit: emitPhpScopeCaptures,
fixturePrefix: 'php',
exts: ['.php'],
file: 'bench.php',
header: '<?php\n\n',
unit: (n) =>
`class Entity${n} {\n` +
` public $id;\n public $name;\n` +
` function getId() { return $this->id; }\n` +
` function setName($v) { $this->name = $v; }\n}\n\n`,
},
{
name: 'ruby',
emit: emitRubyScopeCaptures,
fixturePrefix: 'ruby',
exts: ['.rb'],
file: 'bench.rb',
header: '',
unit: (n) =>
`class Entity${n}\n` +
` def get_id\n @id\n end\n` +
` def set_name(v)\n @name = v\n end\nend\n\n`,
},
{
name: 'cobol',
emit: emitCobolScopeCaptures,
fixturePrefix: 'cobol',
exts: ['.cbl', '.cpy'],
file: 'bench.cbl',
header:
' IDENTIFICATION DIVISION.\n' +
' PROGRAM-ID. BENCH.\n' +
' PROCEDURE DIVISION.\n',
unit: (n) => ` PARA-${String(n).padStart(5, '0')}.\n DISPLAY "P${n}".\n`,
},
];
function generate(lang, entityCount) {
let src = lang.header;
for (let i = 0; i < entityCount; i++) src += lang.unit(i);
return src;
}
// ---- timing ----
function median(xs) {
const s = [...xs].sort((a, b) => a - b);
const m = Math.floor(s.length / 2);
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
function timeEmit(emit, src, file, reps) {
emit(src, `warmup-${file}`); // warm parser/query JIT (not counted)
const samples = [];
let count = 0;
for (let i = 0; i < reps; i++) {
const start = process.hrtime.bigint();
const out = emit(src, file);
samples.push(Number(process.hrtime.bigint() - start) / 1e6);
count = out.length;
}
return { ms: median(samples), count };
}
const SMALL = 250;
const LARGE = 800;
const REPS = 7;
function measureLang(lang) {
// Correctness fingerprint over the fixture corpus + a fixed 20-entity source.
const perFixture = [];
let groups = 0;
for (const { key, absPath } of collectFixtures(lang.fixturePrefix, lang.exts)) {
const matches = lang.emit(fs.readFileSync(absPath, 'utf8'), absPath);
groups += matches.length;
perFixture.push(`${key}\t${matches.length}\t${digestCaptures(matches)}`);
}
const daoMatches = lang.emit(generate(lang, 20), `synthetic-dao-20${path.extname(lang.file)}`);
groups += daoMatches.length;
perFixture.push(`synthetic:dao-20\t${daoMatches.length}\t${digestCaptures(daoMatches)}`);
const fingerprint = crypto
.createHash('sha256')
.update(perFixture.sort().join('\n'))
.digest('hex');
// Scaling.
const small = timeEmit(lang.emit, generate(lang, SMALL), lang.file, REPS);
const large = timeEmit(lang.emit, generate(lang, LARGE), lang.file, REPS);
const scalingRatio = small.ms > 0 ? large.ms / small.ms / (LARGE / SMALL) : 0;
return {
language: lang.name,
elapsed_ms_small: Number(small.ms.toFixed(2)),
elapsed_ms_large: Number(large.ms.toFixed(2)),
scaling_ratio: Number(scalingRatio.toFixed(3)),
capture_groups_small: small.count,
capture_groups_large: large.count,
fingerprint,
capture_groups_fp: groups,
fixture_count: perFixture.length,
};
}
// ---- run ----
const CHECK = process.argv.includes('--check');
const results = LANGS.map(measureLang);
if (!CHECK) {
for (const r of results) process.stdout.write(JSON.stringify(r) + '\n');
} else {
const baselines = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'));
const failures = [];
for (const r of results) {
const base = baselines[r.language];
if (base === undefined) {
failures.push(`${r.language}: no baseline recorded`);
continue;
}
if (r.fingerprint !== base.fingerprint) {
failures.push(
`${r.language}: capture fingerprint drift (got ${r.fingerprint}, expected ${base.fingerprint})`,
);
}
if (r.scaling_ratio >= base.scaling_budget) {
failures.push(
`${r.language}: scaling ratio ${r.scaling_ratio} >= budget ${base.scaling_budget} ` +
`(${SMALL}->${LARGE} ms ${r.elapsed_ms_small}->${r.elapsed_ms_large})`,
);
}
process.stdout.write(JSON.stringify(r) + '\n');
}
if (failures.length > 0) {
for (const f of failures) process.stderr.write(`[scope-capture --check] FAIL: ${f}\n`);
process.exit(1);
}
process.stderr.write(`[scope-capture --check] PASS (${results.length} languages)\n`);
}

View file

@ -25,7 +25,7 @@ import { tryResolveWithExtensions } from './utils.js';
export function resolvePythonImportInternal(
currentFile: string,
importPath: string,
allFiles: Set<string>,
allFiles: ReadonlySet<string>,
): string | null {
// Relative import — PEP 328 (https://peps.python.org/pep-0328/)
if (importPath.startsWith('.')) {

View file

@ -57,7 +57,10 @@ export const EXTENSIONS = [
* Try to match a path (with extensions) against the known file set.
* Returns the matched file path or null.
*/
export function tryResolveWithExtensions(basePath: string, allFiles: Set<string>): string | null {
export function tryResolveWithExtensions(
basePath: string,
allFiles: ReadonlySet<string>,
): string | null {
for (const ext of EXTENSIONS) {
const candidate = basePath + ext;
if (allFiles.has(candidate)) return candidate;

View file

@ -15,7 +15,7 @@ import {
computeCppCallArity,
} from './arity-metadata.js';
import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-linkage.js';
import { markCppDependentBase } from './two-phase-lookup.js';
import { markCppDependentBase, markCppDependentPackBase } from './two-phase-lookup.js';
import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './adl.js';
import { markCppInlineNamespaceRange } from './inline-namespaces.js';
import { extractCppTemplateConstraints } from './constraint-extractor.js';
@ -410,7 +410,7 @@ export function emitCppScopeCaptures(
// captures consumed by the registry-primary graph bridge. The lookup name
// is normalized to the bare class name so `Base<T>` / `outer::v1::Base<T>`
// resolve through V1's simple-name `findClassBindingInScope('Base')`.
emitCppInheritanceCaptures(tree.rootNode, out);
emitCppInheritanceCaptures(tree.rootNode, out, filePath);
// ── Detect dependent-base relationships for two-phase template lookup ──
// Walk the tree once, finding every `template_declaration` whose
@ -459,7 +459,7 @@ function isCppUnsupportedReturnTypeDeclarator(funcDeclarator: SyntaxNode): boole
* here instead of introducing a C++-only name-resolution lane in shared
* ingestion infrastructure.
*/
function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[]): void {
function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[], filePath: string): void {
const stack: SyntaxNode[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
@ -467,11 +467,15 @@ function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[]): void
const baseClause = findChildOfType(node, ['base_class_clause']);
if (baseClause !== null) {
for (const base of iterBaseClasses(baseClause)) {
const baseName = extractBaseLookupName(base);
if (base.isPackExpansion) {
markClassWithPackExpandedBase(filePath, node);
continue;
}
const baseName = extractBaseLookupName(base.node);
if (baseName.length === 0) continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': syntheticCapture('@reference.name', base, baseName),
'@reference.inherits': nodeToCapture('@reference.inherits', base.node),
'@reference.name': syntheticCapture('@reference.name', base.node, baseName),
});
}
}
@ -512,9 +516,12 @@ function detectCppDependentBases(root: SyntaxNode, filePath: string): void {
const baseClause = findChildOfType(classNode, ['base_class_clause']);
if (baseClause !== null) {
for (const base of iterBaseClasses(baseClause)) {
if (isBaseDependent(base, params)) {
const baseName = extractBaseLookupName(base);
const baseQualifier = extractBaseLookupQualifier(base);
if (base.isPackExpansion || isBaseDependent(base.node, params)) {
if (base.isPackExpansion) {
markClassWithPackExpandedBase(filePath, classNode);
}
const baseName = extractBaseLookupName(base.node);
const baseQualifier = extractBaseLookupQualifier(base.node);
if (baseName !== '') {
markCppDependentBase(filePath, className, baseName, baseQualifier);
}
@ -563,8 +570,18 @@ function collectTemplateParameterNames(templateDecl: SyntaxNode): Set<string> {
return names;
}
function markClassWithPackExpandedBase(filePath: string, classNode: SyntaxNode): void {
const className = getTypeIdentifierName(classNode);
if (className !== '') markCppDependentPackBase(filePath, className);
}
interface CppBaseClassEntry {
readonly node: SyntaxNode;
readonly isPackExpansion: boolean;
}
/** Yield each base-class entry from a `base_class_clause`. */
function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator<SyntaxNode> {
function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator<CppBaseClassEntry> {
for (let i = 0; i < baseClause.childCount; i++) {
const child = baseClause.child(i);
if (child === null) continue;
@ -575,11 +592,22 @@ function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator<SyntaxNode>
child.type === 'template_type' ||
child.type === 'qualified_identifier'
) {
yield child;
yield { node: child, isPackExpansion: isFollowedByPackExpansion(baseClause, i) };
}
}
}
function isFollowedByPackExpansion(baseClause: SyntaxNode, childIndex: number): boolean {
for (let i = childIndex + 1; i < baseClause.childCount; i++) {
const sibling = baseClause.child(i);
if (sibling === null) continue;
if (sibling.type === '...' || (!sibling.isNamed && sibling.text === '...')) return true;
if (sibling.type === ',' || sibling.type === 'access_specifier') return false;
if (sibling.isNamed) return false;
}
return false;
}
/**
* A base is dependent when:
* - it's a `template_type` and its argument list contains a

View file

@ -46,6 +46,13 @@ import { findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js';
*/
const dependentBasesByFile = new Map<string, Map<string, Map<string, Set<string>>>>();
/**
* Class templates with pack-expanded bases (`struct Mix : Bases...`) have
* an unknown set of base classes. Unqualified member lookup inside the class
* cannot safely bind to class-owned methods outside the current class.
*/
const dependentPackBaseClassesByFile = new Map<string, Set<string>>();
/**
* Post-`populateOwners` resolution: per-class-nodeId, the set of
* dependent-base-class nodeIds. Built by `populateCppDependentBases`
@ -88,9 +95,19 @@ export function markCppDependentBase(
quals.add(qualifier);
}
export function markCppDependentPackBase(filePath: string, className: string): void {
let perFile = dependentPackBaseClassesByFile.get(filePath);
if (perFile === undefined) {
perFile = new Set();
dependentPackBaseClassesByFile.set(filePath, perFile);
}
perFile.add(className);
}
/** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */
export function clearCppDependentBases(): void {
dependentBasesByFile.clear();
dependentPackBaseClassesByFile.clear();
dependentBaseNodeIds.clear();
}
@ -110,7 +127,7 @@ export function clearCppDependentBases(): void {
* found (conservative: avoids false associations).
*/
export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): void {
if (dependentBasesByFile.size === 0) return;
if (dependentBasesByFile.size === 0 && dependentPackBaseClassesByFile.size === 0) return;
// Build workspace-wide index: simpleName → {nodeId, nsPrefix}[]
// nsPrefix is the dot-joined namespace path (qualifiedName without the
@ -164,6 +181,16 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v
localClassByName.set(simple, { nodeId: def.nodeId, nsPrefix });
}
const packBaseClasses = dependentPackBaseClassesByFile.get(filePath);
if (packBaseClasses !== undefined) {
for (const className of packBaseClasses) {
const classEntry = localClassByName.get(className);
if (classEntry !== undefined) {
dependentBaseNodeIds.set(classEntry.nodeId, new Set(['*pack-expansion*']));
}
}
}
// V3: qualifier-based exact targeting. When the base specifier carries
// a syntactic qualifier (e.g., `detail` in `detail::Inner<T>`), compute
// the expected namespace prefix and use exact (===) match. Falls back to
@ -270,10 +297,31 @@ export function isCppDependentBaseMember(
candidateDef: SymbolDefinition,
scopes: ScopeResolutionIndexes,
): boolean {
if (candidateDef.ownerId === undefined) return false;
const enclosing = findEnclosingClassDef(callerScopeId, scopes);
if (enclosing === undefined) return false;
const bases = dependentBaseNodeIds.get(enclosing.nodeId);
if (bases === undefined) return false;
if (bases.has('*pack-expansion*')) {
if (candidateDef.ownerId !== undefined) return candidateDef.ownerId !== enclosing.nodeId;
if (candidateDef.type !== 'Method' && candidateDef.type !== 'Constructor') return false;
const ownerName = getQualifiedParentName(candidateDef.qualifiedName);
const enclosingName = getQualifiedSimpleName(enclosing.qualifiedName);
return ownerName !== undefined && ownerName !== enclosingName;
}
if (candidateDef.ownerId === undefined) return false;
return bases.has(candidateDef.ownerId);
}
function getQualifiedParentName(qualifiedName: string | undefined): string | undefined {
if (qualifiedName === undefined) return undefined;
const lastDot = qualifiedName.lastIndexOf('.');
if (lastDot < 0) return undefined;
const parent = qualifiedName.slice(0, lastDot);
return getQualifiedSimpleName(parent);
}
function getQualifiedSimpleName(qualifiedName: string | undefined): string | undefined {
if (qualifiedName === undefined) return undefined;
const lastDot = qualifiedName.lastIndexOf('.');
return lastDot >= 0 ? qualifiedName.slice(lastDot + 1) : qualifiedName;
}

View file

@ -17,7 +17,7 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { nodeIfType, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { splitUsingDirective } from './import-decomposer.js';
import { computeCsharpArityMetadata } from './arity-metadata.js';
import { synthesizeCsharpReceiverBinding } from './receiver-binding.js';
@ -103,9 +103,16 @@ export function emitCsharpScopeCaptures(
// `@`; we put it back so the central extractor's prefix lookups
// (`@scope.`, `@declaration.`, …) work.
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map: the query hands us each matched
// node as c.node, so anchors resolve via a type-guarded lookup (nodeIfType)
// instead of re-deriving them with findNodeAtRange(tree.rootNode, ...) per
// match — the O(matches x rootChildren) root-walk fixed for go #1915 /
// python #1918, mirrored here.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
@ -113,8 +120,7 @@ export function emitCsharpScopeCaptures(
// the kind/source/name/alias markers it consumes. Raw query match
// only carries the @import.statement anchor.
if (grouped['@import.statement'] !== undefined) {
const stmtCapture = grouped['@import.statement'];
const stmtNode = findNodeAtRange(tree.rootNode, stmtCapture.range, 'using_directive');
const stmtNode = nodeIfType(nodeMap['@import.statement'], 'using_directive');
if (stmtNode !== null) {
const decomposed = splitUsingDirective(stmtNode);
if (decomposed !== null) {
@ -129,8 +135,7 @@ export function emitCsharpScopeCaptures(
}
if (grouped['@reference.read.member'] !== undefined) {
const anchor = grouped['@reference.read.member'];
const memberNode = findNodeAtRange(tree.rootNode, anchor.range, 'member_access_expression');
const memberNode = nodeIfType(nodeMap['@reference.read.member'], 'member_access_expression');
if (memberNode === null || !shouldEmitReadMember(memberNode)) {
continue;
}
@ -144,8 +149,7 @@ export function emitCsharpScopeCaptures(
// `@scope.function` matches.
if (grouped['@scope.function'] !== undefined) {
out.push(grouped);
const anchor = grouped['@scope.function']!;
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
const fnNode = nodeIfType(nodeMap['@scope.function'], ...FUNCTION_NODE_TYPES);
if (fnNode !== null) {
for (const synth of synthesizeCsharpReceiverBinding(fnNode)) {
out.push(synth);
@ -160,8 +164,7 @@ export function emitCsharpScopeCaptures(
// the first tag that matches.
const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
if (declTag !== undefined) {
const anchor = grouped[declTag]!;
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
const fnNode = nodeIfType(nodeMap[declTag], ...FUNCTION_NODE_TYPES);
if (fnNode !== null) {
const arity = computeCsharpArityMetadata(fnNode);
if (arity.parameterCount !== undefined) {
@ -198,10 +201,11 @@ export function emitCsharpScopeCaptures(
['@reference.call.free', '@reference.call.member', '@reference.call.constructor'] as const
).find((t) => grouped[t] !== undefined);
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
const anchor = grouped[callTag]!;
const callNode =
findNodeAtRange(tree.rootNode, anchor.range, 'invocation_expression') ??
findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
const callNode = nodeIfType(
nodeMap[callTag],
'invocation_expression',
'object_creation_expression',
);
if (callNode !== null) {
const argList = callNode.childForFieldName('arguments');
const args =
@ -240,10 +244,11 @@ export function emitCsharpScopeCaptures(
grouped['@declaration.class'] !== undefined ||
grouped['@declaration.record'] !== undefined
) {
const anchor = grouped['@declaration.class'] ?? grouped['@declaration.record']!;
const typeNode =
findNodeAtRange(tree.rootNode, anchor.range, 'class_declaration') ??
findNodeAtRange(tree.rootNode, anchor.range, 'record_declaration');
const typeNode = nodeIfType(
nodeMap['@declaration.class'] ?? nodeMap['@declaration.record'],
'class_declaration',
'record_declaration',
);
if (typeNode !== null) {
const synth = synthesizePrimaryConstructor(typeNode);
if (synth !== null) out.push(synth);
@ -391,14 +396,3 @@ function inferArgType(argNode: SyntaxNode): string {
return '';
}
}
/** Find the first C# function-like node at the given range. The
* declaration anchor range covers the whole method/constructor/etc.
* node, but the tag alone doesn't tell us which node type. */
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
for (const nodeType of FUNCTION_NODE_TYPES) {
const n = findNodeAtRange(rootNode, range, nodeType);
if (n !== null) return n as SyntaxNode;
}
return null;
}

View file

@ -32,7 +32,7 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { nodeIfType, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { splitNamespaceUseDeclaration } from './import-decomposer.js';
import { computePhpArityMetadata } from './arity-metadata.js';
import { synthesizePhpReceiverBinding } from './receiver-binding.js';
@ -102,9 +102,16 @@ export function emitPhpScopeCaptures(
// Group captures by their tag name. Tree-sitter strips the leading
// `@`; we put it back so the central extractor's prefix lookups work.
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map: the query hands us each matched
// node as c.node, so anchors resolve via a type-guarded lookup (nodeIfType)
// instead of re-deriving them with findNodeAtRange(tree.rootNode, ...) per
// match — the O(matches x rootChildren) root-walk fixed for go #1915 /
// python #1918, mirrored here.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
@ -175,12 +182,7 @@ export function emitPhpScopeCaptures(
// Decompose each `namespace_use_declaration` so `interpretPhpImport`
// sees the kind/source/name/alias markers it consumes.
if (grouped['@import.statement'] !== undefined) {
const stmtCapture = grouped['@import.statement'];
const stmtNode = findNodeAtRange(
tree.rootNode,
stmtCapture.range,
'namespace_use_declaration',
);
const stmtNode = nodeIfType(nodeMap['@import.statement'], 'namespace_use_declaration');
if (stmtNode !== null) {
const decomposed = splitNamespaceUseDeclaration(stmtNode);
if (decomposed.length > 0) {
@ -197,8 +199,7 @@ export function emitPhpScopeCaptures(
// non-static method-like. Mirrors C#'s `this` / `base` synthesis.
if (grouped['@scope.function'] !== undefined) {
out.push(grouped);
const anchor = grouped['@scope.function']!;
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
const fnNode = nodeIfType(nodeMap['@scope.function'], ...FUNCTION_NODE_TYPES);
if (fnNode !== null) {
for (const synth of synthesizePhpReceiverBinding(fnNode)) {
out.push(synth);
@ -219,8 +220,7 @@ export function emitPhpScopeCaptures(
// registry can narrow overloads.
const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
if (declTag !== undefined) {
const anchor = grouped[declTag]!;
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
const fnNode = nodeIfType(nodeMap[declTag], ...FUNCTION_NODE_TYPES);
if (fnNode !== null) {
const arity = computePhpArityMetadata(fnNode);
if (arity.parameterCount !== undefined) {
@ -255,13 +255,14 @@ export function emitPhpScopeCaptures(
['@reference.call.free', '@reference.call.member', '@reference.call.constructor'] as const
).find((t) => grouped[t] !== undefined);
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
const anchor = grouped[callTag]!;
const callNode =
findNodeAtRange(tree.rootNode, anchor.range, 'function_call_expression') ??
findNodeAtRange(tree.rootNode, anchor.range, 'member_call_expression') ??
findNodeAtRange(tree.rootNode, anchor.range, 'nullsafe_member_call_expression') ??
findNodeAtRange(tree.rootNode, anchor.range, 'scoped_call_expression') ??
findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
const callNode = nodeIfType(
nodeMap[callTag],
'function_call_expression',
'member_call_expression',
'nullsafe_member_call_expression',
'scoped_call_expression',
'object_creation_expression',
);
if (callNode !== null) {
const argList = callNode.childForFieldName('arguments');
const args: SyntaxNode[] = [];
@ -293,15 +294,6 @@ export function emitPhpScopeCaptures(
return out;
}
/** Find the first PHP function-like node at the given range. */
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
for (const nodeType of FUNCTION_NODE_TYPES) {
const n = findNodeAtRange(rootNode, range, nodeType);
if (n !== null) return n as SyntaxNode;
}
return null;
}
// ─── PHP receiver normalization ──────────────────────────────────────────────
/**

View file

@ -17,7 +17,7 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { nodeToCapture, syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
import { splitImportStatement } from './import-decomposer.js';
import { getPythonParser, getPythonScopeQuery } from './query.js';
import { synthesizeReceiverTypeBinding } from './receiver-binding.js';
@ -66,21 +66,30 @@ export function emitPythonScopeCaptures(
// `@`; we put it back so the central extractor's prefix lookups
// (`@scope.`, `@declaration.`, …) work.
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
// hands us each matched node as `c.node`, so anchor nodes can be used
// directly (or via a bounded LOCAL walk) instead of re-deriving them with
// `findNodeAtRange(tree.rootNode, ...)`, which scanned all of root's named
// children on every match -> O(matches x rootChildren). That was the #1848
// hotpath in Go (fixed in eaf0a305); the same shape lived here in Python.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
if (grouped['@import.statement'] !== undefined) {
// Decompose multi-name imports. Both `import_statement` and
// `import_from_statement` share the matched range, so we try the
// `from` form first and fall back to plain.
const stmtCapture = grouped['@import.statement'];
const stmtNode =
findNodeAtRange(tree.rootNode, stmtCapture.range, 'import_from_statement') ??
findNodeAtRange(tree.rootNode, stmtCapture.range, 'import_statement');
if (stmtNode !== null) {
// `@import.statement` is captured directly ON the `import_statement` /
// `import_from_statement` node (query: `(import_statement) @import.statement`
// and `(import_from_statement) @import.statement`), so the captured node IS
// the one the old findNodeAtRange re-derived. `splitImportStatement`
// dispatches on those two types; a captured node of any other type would
// have made the old range+type lookup return null -> the defensive raw
// fallback, which the type guard below reproduces exactly.
const stmtNode = nodeMap['@import.statement']!;
if (stmtNode.type === 'import_from_statement' || stmtNode.type === 'import_statement') {
for (const piece of splitImportStatement(stmtNode)) out.push(piece);
} else {
// Defensive fallback: emit the raw match.
@ -91,11 +100,11 @@ export function emitPythonScopeCaptures(
if (grouped['@scope.function'] !== undefined) {
out.push(grouped);
const fnNode = findNodeAtRange(
tree.rootNode,
grouped['@scope.function']!.range,
'function_definition',
);
// `@scope.function` is captured directly on the `function_definition`
// node (query: `(function_definition) @scope.function`), so it IS the
// node the old findNodeAtRange re-derived at that range.
const scopeNode = nodeMap['@scope.function']!;
const fnNode = scopeNode.type === 'function_definition' ? scopeNode : null;
if (fnNode !== null) {
const synth = synthesizeReceiverTypeBinding(fnNode);
if (synth !== null) out.push(synth);
@ -110,7 +119,11 @@ export function emitPythonScopeCaptures(
// The anchor range is the function_definition itself — we resolve
// the node and pipe it through the arity helper.
const anchorCap = grouped['@declaration.function']!;
const fnNode = findNodeAtRange(tree.rootNode, anchorCap.range, 'function_definition');
// `@declaration.function` is captured directly on the `function_definition`
// node (query: `(function_definition name: (identifier) @declaration.name)
// @declaration.function`), so use the captured node, not a root re-walk.
const anchorNode = nodeMap['@declaration.function']!;
const fnNode = anchorNode.type === 'function_definition' ? anchorNode : null;
if (fnNode !== null) {
if (pythonFunctionDefinitionLabel(fnNode, 'Function') === 'Method') {
delete grouped['@declaration.function'];

View file

@ -12,14 +12,14 @@
import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
import { resolvePythonImportInternal } from '../../import-resolvers/python.js';
import { recordPythonFileIndexBuild } from './index-stats.js';
export interface PythonResolveContext {
readonly fromFile: string;
/** Mutable `Set` because the legacy `resolvePythonImportInternal`
* chain downstream is typed to accept `Set<string>`. Callers that
* only hold a `ReadonlySet` should copy via `new Set(...)` at the
* adapter boundary. */
readonly allFilePaths: Set<string>;
/** `ReadonlySet` so the orchestrator's stable run-level set flows straight
* through to `getPythonFileIndex`'s `WeakMap` key (built once per run, not
* copied per import). The whole resolver chain only reads the set. */
readonly allFilePaths: ReadonlySet<string>;
}
export function resolvePythonImportTarget(
@ -31,10 +31,17 @@ export function resolvePythonImportTarget(
// PythonResolveContext-shaped object; narrow structurally rather
// than via a cast chain so unexpected shapes return null cleanly.
const ctx = workspaceIndex as PythonResolveContext | undefined;
// Duck-type the set rather than `instanceof Set`: `allFilePaths` is typed
// `ReadonlySet<string>` and the chain only ever calls `.has()` + iterates, so
// any set-like is valid. An `instanceof Set` check would reject a legitimate
// non-`Set` `ReadonlySet` implementation and silently return null for every
// import (PR #1918 tri-review P2).
const allFilePaths = (ctx as { allFilePaths?: unknown } | undefined)?.allFilePaths;
if (
ctx === undefined ||
typeof (ctx as { fromFile?: unknown }).fromFile !== 'string' ||
!((ctx as { allFilePaths?: unknown }).allFilePaths instanceof Set)
typeof (allFilePaths as { has?: unknown } | undefined)?.has !== 'function' ||
typeof (allFilePaths as Iterable<string> | undefined)?.[Symbol.iterator] !== 'function'
) {
return null;
}
@ -103,7 +110,7 @@ export function resolvePythonImportTarget(
*/
function resolveAbsoluteFromFiles(
pathLike: string,
allFilePaths: Set<string>,
allFilePaths: ReadonlySet<string>,
fromFile: string,
): string | null {
const directFile = `${pathLike}.py`;
@ -145,12 +152,27 @@ function resolveAbsoluteFromFiles(
// multi-directory collision repos.
const suffixFile = `/${directFile}`;
const suffixPkg = `/${directPkg}`;
// Indexed suffix gather. A file matching `…/<pathLike>.py` has basename
// `<lastSeg>.py`; one matching `…/<pathLike>/__init__.py` has basename
// `__init__.py`. Look up only those basename buckets and confirm the full
// suffix, instead of scanning every file (the O(imports x files) hotpath).
// The candidate SET is identical to the old full scan, and the tie-break
// sort below fully determines the result, so output is unchanged. The
// shared buildSuffixIndex is deliberately NOT used: it keeps only one
// path per suffix (longest wins) and so cannot reproduce this exact
// fewest-segments-then-lexicographic tie-break across all candidates.
const index = getPythonFileIndex(allFilePaths);
const lastSeg = pathLike.slice(pathLike.lastIndexOf('/') + 1);
const matches: { raw: string; norm: string }[] = [];
for (const raw of allFilePaths) {
const norm = raw.replace(/\\/g, '/');
if (norm.endsWith(suffixFile) || norm.endsWith(suffixPkg)) {
matches.push({ raw, norm });
}
for (const cand of index.byBasename.get(`${lastSeg}.py`) ?? []) {
if (cand.norm.endsWith(suffixFile)) matches.push(cand);
}
// Package form: only `__init__.py` files whose parent dir is named `<lastSeg>`
// can match `…/<lastSeg>/__init__.py` — look them up by parent key (P2b) and
// confirm the full suffix. Same final candidate set as the old `__init__.py`
// scan, just without iterating unrelated packages.
for (const cand of index.byInitParent.get(`${lastSeg}/__init__.py`) ?? []) {
if (cand.norm.endsWith(suffixPkg)) matches.push(cand);
}
if (matches.length === 0) return null;
if (matches.length === 1) return matches[0].raw;
@ -188,7 +210,7 @@ function resolveAbsoluteFromFiles(
*/
function hasRepoCandidate(
leadingSegment: string,
allFilePaths: Set<string>,
allFilePaths: ReadonlySet<string>,
fromFile: string,
): boolean {
const prefix = `${leadingSegment}/`;
@ -205,15 +227,117 @@ function hasRepoCandidate(
ancestorPrefixes.push(`${dirParts.slice(0, i).join('/')}/${leadingSegment}/`);
}
for (const raw of allFilePaths) {
const f = raw.replace(/\\/g, '/');
if (f === rootFile || f === initFile) return true;
if (f.startsWith(prefix) && f.endsWith('.py')) return true;
if (f.endsWith('.py')) {
for (const ap of ancestorPrefixes) {
if (f.startsWith(ap)) return true;
}
}
// Indexed equivalents of the old O(files) scan:
// (1) `f === rootFile || f === initFile` -> normalized-path membership.
// (2) `f.startsWith(`${seg}/`) && f.endsWith('.py')` -> some .py file lives
// under directory `${seg}/`, i.e. `${seg}/` is a known .py dir prefix.
// (3) ancestor namespace case -> `${ancestor}/${seg}/` is a known .py dir
// prefix.
const index = getPythonFileIndex(allFilePaths);
if (index.normSet.has(rootFile) || index.normSet.has(initFile)) return true;
if (index.dirPrefixes.has(prefix)) return true;
for (const ap of ancestorPrefixes) {
if (index.dirPrefixes.has(ap)) return true;
}
return false;
}
/**
* Per-file-set index for Python import resolution, memoized on the
* `allFilePaths` Set object (the same Set is passed for every import in a run,
* so the index is built once and reused). Replaces the per-import O(files)
* scans in `resolveAbsoluteFromFiles` (suffix match) and `hasRepoCandidate`
* (package-existence gate) with O(1)/O(bucket) lookups.
*
* - `normSet`: every file path, normalized to forward slashes (for the exact
* `f === rootFile|initFile` membership checks).
* - `byBasename`: last path component (e.g. `models.py`, `__init__.py`) ->
* all `{ raw, norm }` candidates, so suffix matches can be gathered from the
* relevant bucket and the exact tie-break applied across ALL of them.
* - `byInitParent`: `__init__.py` files keyed by their last TWO components
* (`<parentDir>/__init__.py`). The package suffix lookup (`pkg.sub` ->
* `…/sub/__init__.py`) targets only same-named package dirs via this map
* instead of scanning every `__init__.py` in the repo — the common
* multi-segment import path no longer scales with package count
* (PR #1918 review P2b). `__init__.py` files stay in `byBasename` too, for
* the rarer explicit `pkg.__init__` import that resolves via the module
* (`…<lastSeg>.py`) lookup.
* - `dirPrefixes`: every directory prefix of a `.py` file, trailing-slashed
* (`a/b/c.py` -> `a/`, `a/b/`), for "is there a .py file under `<dir>/`".
*/
interface PythonFileIndex {
readonly normSet: Set<string>;
readonly byBasename: Map<string, { raw: string; norm: string }[]>;
readonly byInitParent: Map<string, { raw: string; norm: string }[]>;
readonly dirPrefixes: Set<string>;
}
const PYTHON_FILE_INDEX_CACHE = new WeakMap<ReadonlySet<string>, PythonFileIndex>();
function getPythonFileIndex(allFilePaths: ReadonlySet<string>): PythonFileIndex {
const cached = PYTHON_FILE_INDEX_CACHE.get(allFilePaths);
if (cached !== undefined) return cached;
// Cache miss: materialize a fresh index. Counted so a test can assert this
// happens once per run, not once per import (PR #1918 review P1 guard).
recordPythonFileIndexBuild();
const normSet = new Set<string>();
const byBasename = new Map<string, { raw: string; norm: string }[]>();
const byInitParent = new Map<string, { raw: string; norm: string }[]>();
const dirPrefixes = new Set<string>();
for (const raw of allFilePaths) {
const norm = raw.replace(/\\/g, '/');
// Python import resolution only ever queries `.py` paths: module `<seg>.py`
// and package `<seg>/__init__.py` membership (normSet), `<lastSeg>.py` /
// `__init__.py` basename buckets (byBasename), and `.py` directory prefixes
// (dirPrefixes). Non-`.py` files can never match any of those, so skip them
// — they were dead weight in every structure on polyglot monorepos
// (PR #1918 review P3b; dirPrefixes was already `.py`-gated).
if (!norm.endsWith('.py')) continue;
normSet.add(norm);
const lastSlash = norm.lastIndexOf('/');
const base = lastSlash >= 0 ? norm.slice(lastSlash + 1) : norm;
let bucket = byBasename.get(base);
if (bucket === undefined) {
bucket = [];
byBasename.set(base, bucket);
}
bucket.push({ raw, norm });
// Package files also get a parent-keyed bucket so a `pkg.sub` lookup hits
// only `…/sub/__init__.py` candidates, not every `__init__.py` (P2b).
if (base === '__init__.py' && lastSlash >= 0) {
const dir = norm.slice(0, lastSlash);
const parentSlash = dir.lastIndexOf('/');
const parentName = parentSlash >= 0 ? dir.slice(parentSlash + 1) : dir;
if (parentName) {
const initKey = `${parentName}/__init__.py`;
let ib = byInitParent.get(initKey);
if (ib === undefined) {
ib = [];
byInitParent.set(initKey, ib);
}
ib.push({ raw, norm });
}
}
// Directory prefixes: every slash-terminated prefix of the path (every
// index just past a '/', up to and including the file's own directory).
// Scanning the FULL normalized path — including any leading '/' for
// absolute paths — makes `dirPrefixes.has(X)` match exactly when the old
// gate's `f.startsWith(X)` (X always ends in '/') matched. The previous
// split+`filter(Boolean)` dropped the leading empty component, so an
// absolute file `/repo/svc/x.py` yielded `repo/svc/` (no leading slash) and
// gate-passed where `"/repo/svc/x.py".startsWith("repo/svc/")` is false
// (PR #1918 review P3a). For relative paths the set is identical.
for (let i = 0; i <= lastSlash; i++) {
if (norm[i] === '/') dirPrefixes.add(norm.slice(0, i + 1));
}
}
const index: PythonFileIndex = { normSet, byBasename, byInitParent, dirPrefixes };
PYTHON_FILE_INDEX_CACHE.set(allFilePaths, index);
return index;
}

View file

@ -0,0 +1,29 @@
/**
* Build counter for the per-file-set Python import-resolution index
* (`getPythonFileIndex` in `import-target.ts`).
*
* A "build" is a `WeakMap` cache MISS that materializes a fresh
* `PythonFileIndex` (O(files)). Unlike `cache-stats.ts` (which gates its
* counters behind `PROF_SCOPE_RESOLUTION` because they sit on the per-capture
* hot path), this counter is always live: an index build happens at most once
* per resolution run, so the single increment is negligible and an unconditional
* counter avoids env-var load-order fragility in tests.
*
* Used by `test/integration/python-import-index-reuse.test.ts` to assert the
* index is reused across imports (built once per run) rather than rebuilt per
* import — the regression guard for PR #1918 review finding P1.
*/
let INDEX_BUILDS = 0;
export function recordPythonFileIndexBuild(): void {
INDEX_BUILDS++;
}
export function getPythonFileIndexBuildCount(): number {
return INDEX_BUILDS;
}
export function resetPythonFileIndexBuildCount(): void {
INDEX_BUILDS = 0;
}

View file

@ -31,12 +31,13 @@ const pythonScopeResolver: ScopeResolver = {
importEdgeReason: 'python-scope: import',
resolveImportTarget: (targetRaw, fromFile, allFilePaths) => {
// Copy the orchestrator's `ReadonlySet` into a `Set` because the
// legacy Python resolver chain (`resolvePythonImportInternal` →
// `resolveAbsoluteFromFiles` / `hasRepoCandidate`) is typed to
// receive a mutable `Set<string>`. The copy is O(N) but called
// once per import — trivial compared to the parser work.
const ws: PythonResolveContext = { fromFile, allFilePaths: new Set(allFilePaths) };
// Pass the orchestrator's stable run-level `ReadonlySet` straight through
// (no per-import copy). The Python resolver chain only reads the set, and
// `getPythonFileIndex` memoizes its index on the set's identity via a
// WeakMap — so the index is built once per run and reused across every
// import. Copying here (the previous `new Set(allFilePaths)`) handed a
// fresh identity to every import, defeating that cache (PR #1918 review P1).
const ws: PythonResolveContext = { fromFile, allFilePaths };
// `WorkspaceIndex` is an opaque `unknown` placeholder in the
// shared contract, so `ws` passes structurally without a cast.
return resolvePythonImportTarget(

View file

@ -1,6 +1,6 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeIfType,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
@ -49,17 +49,24 @@ export function emitRubyScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The query already hands us each
// matched node as c.node, so anchors are used directly (via nodeIfType)
// instead of re-deriving them with findNodeAtRange(tree.rootNode, ...) per
// match — the O(matches x rootChildren) root-walk fixed for go #1915 /
// python #1918, mirrored here.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
if (tag.startsWith('@_')) continue;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
// Decompose require/require_relative/load into import captures
if (grouped['@import.statement'] !== undefined) {
const anchor = grouped['@import.statement']!;
const callNode = findNodeAtRange(tree.rootNode, anchor.range, 'call');
const callNode = nodeIfType(nodeMap['@import.statement'], 'call');
if (callNode !== null) {
const decomposed = decomposeRubyImport(callNode, anchor);
if (decomposed !== null) {
@ -73,8 +80,7 @@ export function emitRubyScopeCaptures(
// Synthesize self receiver bindings for methods inside class/module
if (grouped['@scope.function'] !== undefined) {
const scopeCap = grouped['@scope.function']!;
const fnNode = findFunctionNode(tree.rootNode, scopeCap.range);
const fnNode = nodeIfType(nodeMap['@scope.function'], ...FUNCTION_NODE_TYPES);
if (fnNode !== null) {
const enclosingNode = findEnclosingClassOrModule(fnNode);
const receiver = synthesizeRubyReceiverBinding(fnNode, enclosingNode);
@ -86,8 +92,7 @@ export function emitRubyScopeCaptures(
// Reclassify declaration.function as declaration.method + attach arity
if (grouped['@declaration.function'] !== undefined) {
const anchorCap = grouped['@declaration.function']!;
const fnNode = findFunctionNode(tree.rootNode, anchorCap.range);
const fnNode = nodeIfType(nodeMap['@declaration.function'], ...FUNCTION_NODE_TYPES);
if (fnNode !== null) {
const enclosingNode = findEnclosingClassOrModule(fnNode);
if (enclosingNode !== null) {
@ -135,11 +140,7 @@ export function emitRubyScopeCaptures(
if (grouped['@reference.call.free'] !== undefined && grouped['@reference.name'] !== undefined) {
const callName = grouped['@reference.name']!.text;
if (HERITAGE_CALL_NAMES.has(callName)) {
const callNode = findNodeAtRange(
tree.rootNode,
grouped['@reference.call.free']!.range,
'call',
);
const callNode = nodeIfType(nodeMap['@reference.call.free'], 'call');
if (callNode !== null) {
const enclosing = findEnclosingClassOrModule(callNode);
const ownerName = enclosing?.childForFieldName('name')?.text;
@ -173,11 +174,7 @@ export function emitRubyScopeCaptures(
// localDefs and gets reconciled into model.fields, enabling write-access
// resolution via receiver-bound-calls (Case 4 → findOwnedMember).
if (ATTR_CALL_NAMES.has(callName)) {
const callNode = findNodeAtRange(
tree.rootNode,
grouped['@reference.call.free']!.range,
'call',
);
const callNode = nodeIfType(nodeMap['@reference.call.free'], 'call');
if (callNode !== null) {
const enclosing = findEnclosingClassOrModule(callNode);
const ownerName = enclosing?.childForFieldName('name')?.text;
@ -222,8 +219,7 @@ export function emitRubyScopeCaptures(
(t) => grouped[t] !== undefined,
);
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
const anchor = grouped[callTag]!;
const callNode = findNodeAtRange(tree.rootNode, anchor.range, 'call');
const callNode = nodeIfType(nodeMap[callTag], 'call');
if (callNode !== null) {
const arity = computeRubyCallArity(callNode);
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(arity));
@ -373,21 +369,37 @@ export function emitRubyScopeCaptures(
// return-type binding `methodName → ClassName` on the method node.
// This enables cross-file return-type propagation for factory methods
// like `def self.get_user; User.new; end` → `get_user → User`.
// Keys of methods that already got a return binding from the YARD pass above,
// precomputed once. The previous `out.some(...)` per method was
// O(methods x out.length) ~ O(n^2); this makes the dedup O(1) per method.
// Key = `<name>:<return-binding startLine>`, matching the old AND condition.
//
// Snapshot-vs-live note (PR #1918 tri-review P3): the old `out.some` was
// evaluated LIVE, so it also saw constructor-return bindings this very loop
// pushed in earlier iterations. That made the old code suppress the 2nd of
// two same-named methods one source row apart whose bodies both end in
// `Const.new` (the 1st's pushed binding startLine == the 2nd's row via the
// 1-based/0-based offset below). The snapshot is built from the YARD pass
// only, so it no longer cross-suppresses — both bindings are emitted, which
// is the intended behavior (the cross-suppression was unintended). This
// corner is absent from fixtures, so the capture fingerprint is unchanged;
// ruby-captures-golden.test.ts pins it explicitly.
const yardReturnKeys = new Set<string>();
for (const m of out) {
const ret = m['@type-binding.return'];
const name = m['@type-binding.name'];
if (ret !== undefined && name !== undefined) {
yardReturnKeys.add(`${name.text}:${ret.range.startLine}`);
}
}
for (const methodNode of [
...tree.rootNode.descendantsOfType('method'),
...tree.rootNode.descendantsOfType('singleton_method'),
]) {
const methodName = methodNode.childForFieldName('name')?.text;
if (methodName === undefined) continue;
// Skip if a YARD @return already created a return binding for this method
if (
out.some(
(m) =>
m['@type-binding.return'] !== undefined &&
m['@type-binding.name']?.text === methodName &&
m['@type-binding.return']?.range.startLine === methodNode.startPosition.row,
)
) {
// Skip if a YARD @return already created a return binding for this method.
if (yardReturnKeys.has(`${methodName}:${methodNode.startPosition.row}`)) {
continue;
}
const body = methodNode.childForFieldName('body');
@ -529,14 +541,6 @@ function computeRubyCallArity(callNode: SyntaxNode): number {
return count;
}
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
for (const nodeType of FUNCTION_NODE_TYPES) {
const n = findNodeAtRange(rootNode, range, nodeType);
if (n !== null) return n;
}
return null;
}
function scopeExtractionError(stage: string, filePath: string, err: unknown): Error {
const reason = err instanceof Error ? err.message : String(err);
return new Error(

View file

@ -1,6 +1,6 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeIfType,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
@ -32,17 +32,23 @@ export function emitRustScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map: the query hands us each matched
// node as c.node, so anchors resolve via a type-guarded lookup (nodeIfType)
// instead of re-deriving them with findNodeAtRange(tree.rootNode, ...) per
// match — the O(matches x rootChildren) root-walk fixed for go #1915 /
// python #1918, mirrored here.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
if (tag.startsWith('@_')) continue;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
// Decompose use declarations into individual import captures
if (grouped['@import.statement'] !== undefined) {
const anchor = grouped['@import.statement']!;
const useNode = findNodeAtRange(tree.rootNode, anchor.range, 'use_declaration');
const useNode = nodeIfType(nodeMap['@import.statement'], 'use_declaration');
if (useNode !== null) {
out.push(...splitRustUseDeclaration(useNode));
continue;
@ -52,8 +58,7 @@ export function emitRustScopeCaptures(
// Synthesize self receiver bindings for methods inside impl blocks
let cachedImplLookup: { fnNode: SyntaxNode; implNode: SyntaxNode | null } | undefined;
if (grouped['@scope.function'] !== undefined) {
const scopeCap = grouped['@scope.function']!;
const fnNode = findNodeAtRange(tree.rootNode, scopeCap.range, 'function_item');
const fnNode = nodeIfType(nodeMap['@scope.function'], 'function_item');
if (fnNode !== null) {
const implNode = findEnclosingImpl(fnNode);
cachedImplLookup = { fnNode, implNode };
@ -65,7 +70,7 @@ export function emitRustScopeCaptures(
// Attach declaration arity for functions/methods
const declAnchor = grouped['@declaration.function'];
if (declAnchor !== undefined) {
const fnNode = findNodeAtRange(tree.rootNode, declAnchor.range, 'function_item');
const fnNode = nodeIfType(nodeMap['@declaration.function'], 'function_item');
if (fnNode !== null) {
const implNode =
cachedImplLookup?.fnNode === fnNode
@ -115,7 +120,7 @@ export function emitRustScopeCaptures(
grouped['@type-binding.name'] !== undefined
) {
const tbReturnAnchor = grouped['@type-binding.return']!;
const fnNode = findNodeAtRange(tree.rootNode, tbReturnAnchor.range, 'function_item');
const fnNode = nodeIfType(nodeMap['@type-binding.return'], 'function_item');
if (fnNode !== null) {
const implNode = findEnclosingImpl(fnNode);
if (implNode !== null) {
@ -141,14 +146,12 @@ export function emitRustScopeCaptures(
}
// Attach call arity for call expressions
const callAnchor =
grouped['@reference.call.free'] ??
grouped['@reference.call.member'] ??
grouped['@reference.call.constructor'];
if (callAnchor !== undefined) {
const callNode =
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'struct_expression');
const callAnchorNode =
nodeMap['@reference.call.free'] ??
nodeMap['@reference.call.member'] ??
nodeMap['@reference.call.constructor'];
if (callAnchorNode !== undefined) {
const callNode = nodeIfType(callAnchorNode, 'call_expression', 'struct_expression');
if (callNode !== null) {
const arity = computeRustCallArity(callNode);
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(arity));

View file

@ -116,11 +116,13 @@ export function emitFreeCallFallback(
// enclosing class. When the workspace has multiple methods of
// the same name in a single class, choose the best match by
// arity + argument types.
let fnDefFromImplicitThis = false;
if (fnDef === undefined) {
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, {
conversionRankFn: options.conversionRankFn,
constraintCompatibility: options.constraintCompatibility,
});
fnDefFromImplicitThis = fnDef !== undefined;
}
// Scope-chain callable lookup. First-match preserves scope-chain
// precedence (local shadows import). When a conversion-rank function
@ -336,6 +338,19 @@ export function emitFreeCallFallback(
);
}
if (fnDef === undefined) continue;
if (
(fnDefFromImplicitThis || fnDef.type === 'Method' || fnDef.type === 'Constructor') &&
options.isCallableVisibleFromCaller !== undefined &&
!options.isCallableVisibleFromCaller({
callerParsed: parsed,
candidate: fnDef,
callerScope: site.inScope,
scopes,
})
) {
handledSites.add(siteKey(parsed.filePath, site));
continue;
}
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
if (callerGraphId === undefined) continue;
const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup);

View file

@ -725,3 +725,23 @@ export function findNodeAtRange(
}
return null;
}
/**
* Return the captured node if its type is one of `types`, else null.
*
* The threaded-node equivalent of `findNodeAtRange(root, capture.range, type)`
* for the common case where a tree-sitter query already hands you the matched
* node (`c.node`): the captured node IS the node at that range, so a type check
* is exact and there is no need to re-walk from the tree root (the
* O(matches × rootChildren) hot path #1848 hit). Unlike `findNodeAtRange`, this
* does NOT traverse — the caller must already hold the node; for a multi-type
* call the node must literally be one of `types` (no fallback search).
*
* Used by every language's scope-capture path (go/python/ruby/php/rust/csharp).
*/
export function nodeIfType<T extends SyntaxNode>(
node: T | undefined,
...types: readonly string[]
): T | null {
return node !== undefined && types.includes(node.type) ? node : null;
}

View file

@ -0,0 +1,634 @@
{
"csharp-alias-imports/Models/Repo.cs": {
"captureGroups": 12,
"digest": "f46b3ec22a53f7aa3409d001e6c845dd9b17d3634f0f791f6b014bd599884309"
},
"csharp-alias-imports/Models/User.cs": {
"captureGroups": 12,
"digest": "065ae045a857e4c881c62ea7234a846332b9d8489f539fa4cfad045707eab039"
},
"csharp-alias-imports/Services/Main.cs": {
"captureGroups": 17,
"digest": "24ce33ab9016af2cb4da1384f4478a3ccde4d856e0c6839222d50e8946a0d9a7"
},
"csharp-ambiguous/Models/Handler.cs": {
"captureGroups": 7,
"digest": "520efd0a0ac67137dbae921eb0d27d4b0f841fc0582bfe01fb664d54bce2fd06"
},
"csharp-ambiguous/Models/IProcessor.cs": {
"captureGroups": 6,
"digest": "d38477ce3dd97a87f38359da966cb7445588fb5c17bdaa7a25bc6f23e8d9d1ec"
},
"csharp-ambiguous/Other/Handler.cs": {
"captureGroups": 7,
"digest": "119e87ba0c830530fdf6fb04d592501c6c552e1051613ebc65c1c1fb0da1ce47"
},
"csharp-ambiguous/Other/IProcessor.cs": {
"captureGroups": 6,
"digest": "ceae1be5aae89de4eec68c5a64195728a7ae434a9b2cab2eaab617b912c3433d"
},
"csharp-ambiguous/Services/UserHandler.cs": {
"captureGroups": 9,
"digest": "f4e4371e9d23c48e9c1a89cc9475bddadffdd3d0bab129690b4b142a4dea94cd"
},
"csharp-assignment-chain/Models/Repo.cs": {
"captureGroups": 7,
"digest": "65fc6c6688e7f7badead653bbef85fca225b6910eeaacb9f30d03cb28aeb1e26"
},
"csharp-assignment-chain/Models/User.cs": {
"captureGroups": 7,
"digest": "4f0c929c82e56f41e5b3b4c0aefee2121d7a307ef24535ac998fd7041d0e025b"
},
"csharp-assignment-chain/Program.cs": {
"captureGroups": 29,
"digest": "81757e8ea3909070375dfea988987f2ae759d9a607498f02ca929112f05533d1"
},
"csharp-async-binding/Order.cs": {
"captureGroups": 8,
"digest": "20a7e738e7e3248ef9fcdc34b460b899fe1dc882dc1577cabacee49c2617af2b"
},
"csharp-async-binding/OrderService.cs": {
"captureGroups": 11,
"digest": "8aefaf0ef949e72fd3dff12bbb9fe7b97f1abe7f53cf3f13bb8ba6691da566e9"
},
"csharp-async-binding/Program.cs": {
"captureGroups": 31,
"digest": "7598dd29b2653f598be6668b0695de630545b90726cbfa8c9fcbe352d0cb20d5"
},
"csharp-async-binding/User.cs": {
"captureGroups": 8,
"digest": "2e80c42ee70d1c96bb4934fecd04ee696c131ec5984afc6f750f8ccf2be16f03"
},
"csharp-async-binding/UserService.cs": {
"captureGroups": 11,
"digest": "d51bfff25e0f3a62962dc61a57e8a642529d47691a5ed75bceaac6b37c48848e"
},
"csharp-call-result-binding/App.cs": {
"captureGroups": 24,
"digest": "da0e6286460e3eb719452574c2c627f3d242394b165525e026926c08b26cb88c"
},
"csharp-calls/Services/UserService.cs": {
"captureGroups": 10,
"digest": "bea2e451e3d28fd3eb172c6ba99f1fb4827929733edef6aaf49545a830caaa54"
},
"csharp-calls/Utils/OneArg.cs": {
"captureGroups": 6,
"digest": "4ca5be18e4c5b9e5cebfecc2424e69d4adf8160a857ded2efc28dc4676965a3d"
},
"csharp-calls/Utils/ZeroArg.cs": {
"captureGroups": 6,
"digest": "02e3a191df1c53fe29d9a463bb65eee0e98ef9d19288678e1fee7ae6c692809d"
},
"csharp-chain-call/Models/Repo.cs": {
"captureGroups": 7,
"digest": "8bab68d412d077c3c4d48d9f95f51847f06d11391cda73e49d528ce58b494b88"
},
"csharp-chain-call/Models/User.cs": {
"captureGroups": 7,
"digest": "1bce064128c35d0799914587b2f4afe7ff69f36a3ab6c6bcf0ead801a7171cd0"
},
"csharp-chain-call/Program.cs": {
"captureGroups": 12,
"digest": "db5c68b0370c594cd006cabb3c836c7a0c5d9ab3a9be8f3cb4420ec76bd31d8a"
},
"csharp-chain-call/Services/UserService.cs": {
"captureGroups": 10,
"digest": "89b0d36671d7a0392a2b2a719efa6c337713f60e4ed7edb6ef83d27c58fbf588"
},
"csharp-child-extends-parent/src/App.cs": {
"captureGroups": 12,
"digest": "817c2f4caf130d8af6ca4bbbc44d25c3186b59e1bddf6cb06b93cd99b98abd0c"
},
"csharp-child-extends-parent/src/Child.cs": {
"captureGroups": 4,
"digest": "18a42204289b0c126ff5eb78497c631c51361b030626639b60e8005d0e4e69c2"
},
"csharp-child-extends-parent/src/Parent.cs": {
"captureGroups": 7,
"digest": "0d8085667a8b8d406588ce115772ada953373dbe8a7c334f5ae17acd0ac01a64"
},
"csharp-class-static-field-access/src/Counters.cs": {
"captureGroups": 13,
"digest": "a01b1b506018b475500beac2bc0662c1f77ac04086eb1b075714ed6360cae9d0"
},
"csharp-collection-accessor/Models/Widget.cs": {
"captureGroups": 7,
"digest": "1289983262ca7a29ad77abb559243eb4ab7dcb7e036563a09989fd4e4898b49c"
},
"csharp-collection-accessor/Services/Renderer.cs": {
"captureGroups": 15,
"digest": "21a4db0b832bd83307b0fbacd1540854157457c95a81c2e351653ff2009a972d"
},
"csharp-deep-field-chain/Models.cs": {
"captureGroups": 24,
"digest": "c14f06f17ae5bad8aaf7ed6c5691a30b119aceeea35016e765c12cf60ba22f13"
},
"csharp-deep-field-chain/Service.cs": {
"captureGroups": 12,
"digest": "0a5a6dfb0a872ebda19856f7e7cf805e1e72000e96ac1db5939c4df0234acc86"
},
"csharp-dictionary-keys-values/App.cs": {
"captureGroups": 19,
"digest": "b445e081f6556e4f84e45f0ad3b4d6f8a4b64ff24a6e53a7ac2109d80fc3225a"
},
"csharp-dictionary-keys-values/Repo.cs": {
"captureGroups": 7,
"digest": "15141c737496213d6a3cac0a3078a9a0ce247e95d901825f3555ac3128198db8"
},
"csharp-dictionary-keys-values/User.cs": {
"captureGroups": 7,
"digest": "49ec4930562a8e12040010b12535a1620aa5ab86e2aab3cad2623e8aec106518"
},
"csharp-field-types/Models.cs": {
"captureGroups": 16,
"digest": "c66da699df823e98eba7ee05170001ed51a7f4ed464ece95c8bb8aa92c2cb5bc"
},
"csharp-field-types/Service.cs": {
"captureGroups": 9,
"digest": "c39485169d15a2b74cc3b67417aab92edf7b6c3a2d64e8e4311b14fb7f5a882e"
},
"csharp-foreach/Models/Repo.cs": {
"captureGroups": 7,
"digest": "65fc6c6688e7f7badead653bbef85fca225b6910eeaacb9f30d03cb28aeb1e26"
},
"csharp-foreach/Models/User.cs": {
"captureGroups": 7,
"digest": "4f0c929c82e56f41e5b3b4c0aefee2121d7a307ef24535ac998fd7041d0e025b"
},
"csharp-foreach/Program.cs": {
"captureGroups": 17,
"digest": "44b01246c74a987b8e0938eeda7609b2b3e068b92f1217e33151c20637eb99d7"
},
"csharp-frozen-binding-collision/App/Program.cs": {
"captureGroups": 17,
"digest": "9e53c57dcddc2ce6d33556c0ab3240cddd7f28647a8d03992c4b082db0f5e5c8"
},
"csharp-frozen-binding-collision/Models/User.cs": {
"captureGroups": 7,
"digest": "d83de96ab701ee20bfac480d507c2d6f1c1e973fa466a9015f2cdf0d9be0803a"
},
"csharp-generic-parent-resolution/src/Models/BaseModel.cs": {
"captureGroups": 7,
"digest": "7773394d9ee9598dc3a9c0992c8ccf411071edde9418ab73d1c21eb5037a108a"
},
"csharp-generic-parent-resolution/src/Models/Repo.cs": {
"captureGroups": 7,
"digest": "03009e02efb0bb1fb33dae6cd15b99c46f2e11a30da79fb8696ec3066c203db0"
},
"csharp-generic-parent-resolution/src/Models/User.cs": {
"captureGroups": 9,
"digest": "62f80db9064341b9ff5f5761113672ec4ceb369af962a94e3b8b4b34c4e26e9c"
},
"csharp-generic-type-refs/Program.cs": {
"captureGroups": 21,
"digest": "833526f5f33d9b648fa22f696a3c2fff9ebfd9e733f567fa13c9607d40faba06"
},
"csharp-grandparent-resolution/Models/A.cs": {
"captureGroups": 9,
"digest": "2687b8e8f0b869d1ea8bdb9cf04dba3845f25ec1699b518114c57c4851b3528d"
},
"csharp-grandparent-resolution/Models/B.cs": {
"captureGroups": 4,
"digest": "0031e28abc076739af04291521c7c5272201c68c60ce5cef68f259a2c10429d4"
},
"csharp-grandparent-resolution/Models/C.cs": {
"captureGroups": 4,
"digest": "8f100ee67304e05401c5021167d36fa7a1fbce9188fd8f1d2fdf9ac0e7a6eb7d"
},
"csharp-grandparent-resolution/Models/Greeting.cs": {
"captureGroups": 7,
"digest": "fa105c430ad5ff6dbf73cd93f219ba42464cab335e8bb3b29a06326e8def17be"
},
"csharp-grandparent-resolution/Services/App.cs": {
"captureGroups": 13,
"digest": "7e386e4731095480c515b8f4a538ea6eb19800345ce4c0e51d6d9307971f2a97"
},
"csharp-hello/Hello.cs": {
"captureGroups": 19,
"digest": "a6e6e37315c555af53aed63767a341bb3b1a4086e64d99b4b073037a4bc0284b"
},
"csharp-interface-default-method/App.cs": {
"captureGroups": 11,
"digest": "3f64ed5bfd0eb0bc62d73b908eebac7025f9e32639aeebc8db9160f3523b521a"
},
"csharp-interface-default-method/User.cs": {
"captureGroups": 10,
"digest": "747fe9fed63700c0ce233b76d2544ed4cae16d68beb4ea29095d24cf68ff8bbe"
},
"csharp-interface-default-method/Validator.cs": {
"captureGroups": 7,
"digest": "27f5051d339f63be01534b002271db6efe647955501129b90d6fd18fb3dac18c"
},
"csharp-interface-dispatch/App.cs": {
"captureGroups": 11,
"digest": "ca1d074837f867a78d9b5e1f197ab967128e89b20d62acdba34bfaa08b01ef6d"
},
"csharp-interface-dispatch/IRepository.cs": {
"captureGroups": 7,
"digest": "14743965ad586b13254a9618686d601b0dd36f3be97531c9fb0605139e83809d"
},
"csharp-interface-dispatch/SqlRepository.cs": {
"captureGroups": 11,
"digest": "57cf94afb053e43647c295d3bd5ba9864a56cc75895d5e37194d2df18f1e744a"
},
"csharp-interface-heritage/src/IAuditableService.cs": {
"captureGroups": 5,
"digest": "eca6d49bcd2d8316ced662b168c4cd379f6f0ce08e591d6fd1ff513e28372a56"
},
"csharp-interface-heritage/src/IBarService.cs": {
"captureGroups": 6,
"digest": "ebd71b8cd792309f85be2bdccb2c99c8ee55a818ffb92791b8f72fbe6a04e791"
},
"csharp-interface-heritage/src/IBaseInterface.cs": {
"captureGroups": 6,
"digest": "cfbc24950b74dd10ddb59c25f8aa4d4b83c5e72502c4227f3f6e311e9e06401f"
},
"csharp-interface-heritage/src/IFooService.cs": {
"captureGroups": 6,
"digest": "19bef355b4992f96f06331ed11d8652b86d25ac1fb5e9cbc2d5d5c0ef85b123d"
},
"csharp-interface-heritage/src/MyService.cs": {
"captureGroups": 18,
"digest": "c565e684ad7e04915bce9fc3124e51db6b41669a20f29b4bc117317506bd7985"
},
"csharp-interface-receiver-static/src/ILogger.cs": {
"captureGroups": 6,
"digest": "846b1d46ba9e45334e2e495f42e9d54ce7b86f36e955877b644f9f52bb5f654b"
},
"csharp-interface-receiver-static/src/Runner.cs": {
"captureGroups": 8,
"digest": "9ab5f0dd02a660099b28b88e289ac2ab724ec7eea662ba535263bd76d17d9bd4"
},
"csharp-is-pattern/models/Repo.cs": {
"captureGroups": 7,
"digest": "bdbacca40f98981bacda770bc3b594f7346956ec512b017981e5dff2528428fa"
},
"csharp-is-pattern/models/User.cs": {
"captureGroups": 7,
"digest": "1a3928a23d3787dd0dc3ff8f4508b06ca67b9cac6f2ff9735e61ff2968fa53f6"
},
"csharp-is-pattern/services/App.cs": {
"captureGroups": 10,
"digest": "669b5e0cbb49ed97ebb0721135f45e2f1e796952c39820d6c03ab9d0d2017aae"
},
"csharp-large-cache-miss-resolution/Models/User.cs": {
"captureGroups": 7,
"digest": "8902fae6ec1e6e6b2bd81e669208dd1277caea531777c9b1e1fef7492b8536f4"
},
"csharp-large-cache-miss-resolution/Other/Helper.cs": {
"captureGroups": 4,
"digest": "2b8a0df5cb14c0c7df3d1dac09e25e8536d3d9ef7ae107570f09f92df3e3f554"
},
"csharp-large-cache-miss-resolution/Services/UserService.cs": {
"captureGroups": 15,
"digest": "711b8e8828d46e47c4ec58fb988f69d119b137610c87d64a0bb0b608855e06a7"
},
"csharp-local-shadow/App/Main.cs": {
"captureGroups": 12,
"digest": "eabfcf7555db7d72f29e5a4680349ff8b3be3ad2ed4247f41c67814e1d03a93a"
},
"csharp-local-shadow/Utils/Logger.cs": {
"captureGroups": 8,
"digest": "1b3b9b6fa338689500910b63444c4460392781db52ce875d760d724d7e6d3953"
},
"csharp-member-calls/Models/User.cs": {
"captureGroups": 7,
"digest": "4f0c929c82e56f41e5b3b4c0aefee2121d7a307ef24535ac998fd7041d0e025b"
},
"csharp-member-calls/Services/UserService.cs": {
"captureGroups": 12,
"digest": "2bbba859b5b5840dd880fac07de97a289be88f6ea11e0eadca652d66cad0b20a"
},
"csharp-method-chain-binding/App.cs": {
"captureGroups": 53,
"digest": "09e0bed66a03cddb567b863b8d916b5b2f4210547caf2f8682c266d36102125e"
},
"csharp-method-enrichment/Animal.cs": {
"captureGroups": 16,
"digest": "a2200a76108b6ef05340718bf7485d762f8004db76191992552cba370eb3999c"
},
"csharp-method-enrichment/App.cs": {
"captureGroups": 14,
"digest": "e51201af749c9f1f15331f0dafceb9c9b18deb63df8da0eb42164a19bbacffbc"
},
"csharp-mixed-decl-chain/Models/Repo.cs": {
"captureGroups": 6,
"digest": "6f4b66e81160e277a84b475e3ff9a27e55dfe5c8f03b370b5304ec1405f055de"
},
"csharp-mixed-decl-chain/Models/User.cs": {
"captureGroups": 6,
"digest": "be74672b6af6192d0b7872dcad9a6ba4af08156aad5cc01c6fe3eb2ee02b4127"
},
"csharp-mixed-decl-chain/Program.cs": {
"captureGroups": 25,
"digest": "4f25b7d6327c0f9d1f4ca9e8d1af07ced3b6fedbfbb8e4996c73cbfb963ef24e"
},
"csharp-namespace-as-root-no-trailing-newline/App/Program.cs": {
"captureGroups": 12,
"digest": "de7d96908b66cbabf13203aa6ffa613e7cb463ed98e1073d98efc0d10a26a11b"
},
"csharp-namespace-as-root-no-trailing-newline/Models/User.cs": {
"captureGroups": 7,
"digest": "c024ea5f5d7dc3ecfe98be3d3b38d18bbe358974bfce69514c2474f5e44d6490"
},
"csharp-nested-member-foreach/App.cs": {
"captureGroups": 19,
"digest": "e4a193bdab13753422287500c3933ddf8fde1290c2250c3fecc838968f3092e9"
},
"csharp-nested-member-foreach/Repo.cs": {
"captureGroups": 7,
"digest": "15141c737496213d6a3cac0a3078a9a0ce247e95d901825f3555ac3128198db8"
},
"csharp-nested-member-foreach/User.cs": {
"captureGroups": 7,
"digest": "49ec4930562a8e12040010b12535a1620aa5ab86e2aab3cad2623e8aec106518"
},
"csharp-no-csproj/Models/User.cs": {
"captureGroups": 10,
"digest": "861d7c13c4460ccce77d898d7720c7ae0d8eef49af4cee9c8fd09f1f1e59b36f"
},
"csharp-no-csproj/Services/UserService.cs": {
"captureGroups": 12,
"digest": "a9fae488a7a5ce393554f0c25ccf6d23ea02c78590ec9fcdb1a93e37515c3587"
},
"csharp-null-check-narrowing/Models/Repo.cs": {
"captureGroups": 7,
"digest": "36f2a415f8e92380a6809045c632fad8185cae70f63d76433a7ffb6dc2f89e72"
},
"csharp-null-check-narrowing/Models/User.cs": {
"captureGroups": 7,
"digest": "81148127c77341020f9841d665aca8b40868f1121eafdb27ddf0265690a6e31f"
},
"csharp-null-check-narrowing/Services/App.cs": {
"captureGroups": 29,
"digest": "64c9dfa8df03023edb1728cecd320d8a0cea67ccb5701e26461a26f08f8c544a"
},
"csharp-null-conditional/App.cs": {
"captureGroups": 16,
"digest": "4c6ac6c2d8e17d4f4b0921dd139947ccb7b4cb950b895e3d6ee939a873918cee"
},
"csharp-null-conditional/Models/Repo.cs": {
"captureGroups": 7,
"digest": "1bd630cb2c4b938d97ee7416ce1542feacd0b6d40163f76cdec7fcf3df59bd0b"
},
"csharp-null-conditional/Models/User.cs": {
"captureGroups": 7,
"digest": "4f0c929c82e56f41e5b3b4c0aefee2121d7a307ef24535ac998fd7041d0e025b"
},
"csharp-optional-params/Services/App.cs": {
"captureGroups": 15,
"digest": "d146e09259dd67a7bc33d1117d4df7a65e760645d024539fcee787db67ed0fdf"
},
"csharp-overload-dispatch/App.cs": {
"captureGroups": 12,
"digest": "c0ac495ae6dc34db5cdb9f508be050fab06920e8f110b14a8c9f095613a269c5"
},
"csharp-overload-dispatch/IRepository.cs": {
"captureGroups": 9,
"digest": "5211301516540daf9c54fb3c8ca3b43bddcce4f81a14034056f2bc897ae925a4"
},
"csharp-overload-dispatch/SqlRepository.cs": {
"captureGroups": 16,
"digest": "7052be187bc4039e4e94c29446c4dae0aed9b288d95c067905a15a33f3d1625d"
},
"csharp-overload-interface/App/Caller.cs": {
"captureGroups": 13,
"digest": "67439018a3750918b66735e67dc4edcef8ffaacd37d97a9efcc3504c492a1a0a"
},
"csharp-overload-interface/App/Logger.cs": {
"captureGroups": 10,
"digest": "1f78db08c2a7a8d3402cf3b0f092ec6aa5b6aa9fedc790d088eaffce8e4bd713"
},
"csharp-overload-interface/Greeting/EnGreeter.cs": {
"captureGroups": 8,
"digest": "f719d5044c243225989e8cbf675d01adeb5cc2023338f320d422ccdab94964ac"
},
"csharp-overload-interface/Greeting/FrGreeter.cs": {
"captureGroups": 8,
"digest": "4e18d58d5a54a87262e42cd84043dc31cbf6d31609eb5a8b3d61c0e692f3ea08"
},
"csharp-overload-interface/Greeting/IGreeter.cs": {
"captureGroups": 6,
"digest": "0de9dec9ee677f1f6ce37654af62bcfab81270c12713d806475397936957eb23"
},
"csharp-overload-param-types/Models/UserService.cs": {
"captureGroups": 27,
"digest": "84cd0161f46cf334a85ed210739df5c40ae8fc7e8d8c7a4cbc3e459d2927ee1c"
},
"csharp-parent-resolution/src/Models/BaseModel.cs": {
"captureGroups": 7,
"digest": "7eb6d696b2699b0b2ed65dbd77061dc3978a469dd412e6ddab2fc17ea273df1d"
},
"csharp-parent-resolution/src/Models/ISerializable.cs": {
"captureGroups": 6,
"digest": "93f2d7c639083aa077ba3c9d2dea678d0d6a2aa914e8078307c98f4c68530226"
},
"csharp-parent-resolution/src/Models/User.cs": {
"captureGroups": 8,
"digest": "ad73f273c29be9536b40c663f6d1864f78b2bca667483d73cc3de4c4900c79ed"
},
"csharp-pattern-matching/Models/Animal.cs": {
"captureGroups": 17,
"digest": "fda2322e993f129e139d11bffe48e2742459792e90b99b8eb0c9a10da15b5139"
},
"csharp-pattern-matching/Services/AnimalService.cs": {
"captureGroups": 11,
"digest": "5418fbf243683914fea8eeed02f2ef856b02ba6c7e361e001da4662eb1332359"
},
"csharp-primary-ctors/App.cs": {
"captureGroups": 14,
"digest": "4c2e190ed675ac78244e0a4230d79e9e4c74f2ef794dd9b8126f6b57a4242f4c"
},
"csharp-primary-ctors/Models/Person.cs": {
"captureGroups": 5,
"digest": "f11b11a31ad7a691a0369d61ce8c067c2d0755515fce4fee2657b6e444c7f21a"
},
"csharp-primary-ctors/Models/User.cs": {
"captureGroups": 10,
"digest": "5233b368adda16fb7573e5aedbd3be0692f166eddff9ee987a0be93c7eb1a4e8"
},
"csharp-proj/Interfaces/IRepository.cs": {
"captureGroups": 12,
"digest": "f3e98fc370c43743c84d82432ed7e5c7039acd0032ee1194d53453ba2c28f132"
},
"csharp-proj/Models/BaseEntity.cs": {
"captureGroups": 8,
"digest": "633570540177423d352b44b0f5230d7f440f101789d1b92192c0fc360e6c26d3"
},
"csharp-proj/Models/User.cs": {
"captureGroups": 18,
"digest": "a9ef60d8f0ad5109369df107c4ce20ac472e5a0d018bc7df2350c2ae66607372"
},
"csharp-proj/Services/UserService.cs": {
"captureGroups": 19,
"digest": "42b79c274955abcd548ca842077e05b0b6b46e324dd29f7a297882ac790ce11f"
},
"csharp-qualified-types/Data/User.cs": {
"captureGroups": 7,
"digest": "16b056de69cc44d953e4cc702f22986b61d935193c4d6bd9cd9a1adcbe2b3e6c"
},
"csharp-qualified-types/Services/User.cs": {
"captureGroups": 7,
"digest": "c3a275cbcdcdd71b6611df01710d0e7d201924aada837f35d8c956207a5a17c4"
},
"csharp-receiver-resolution/App.cs": {
"captureGroups": 18,
"digest": "41bbfa18dc9cce94ce04de96f01381f86e5fedb3d7c6acf3cbcb8de2b45110e8"
},
"csharp-receiver-resolution/Models/Repo.cs": {
"captureGroups": 7,
"digest": "65fc6c6688e7f7badead653bbef85fca225b6910eeaacb9f30d03cb28aeb1e26"
},
"csharp-receiver-resolution/Models/User.cs": {
"captureGroups": 7,
"digest": "4f0c929c82e56f41e5b3b4c0aefee2121d7a307ef24535ac998fd7041d0e025b"
},
"csharp-record-base/src/Models/BaseEntity.cs": {
"captureGroups": 7,
"digest": "e7da2e190dad718eeaa22dad011740fd2086eb2f86331d887e47cd9c5092003c"
},
"csharp-record-base/src/Models/UserRecord.cs": {
"captureGroups": 9,
"digest": "4b7092ef2ded4e37d2fe1591610259e18b9bd3d92ebe32c90eef6683d9b9a0f6"
},
"csharp-recursive-pattern/Models/Repo.cs": {
"captureGroups": 8,
"digest": "089242f5a0e1d4338002f9f72788116e285ee24f719e702bc15a1858e7a04d97"
},
"csharp-recursive-pattern/Models/User.cs": {
"captureGroups": 8,
"digest": "a460c8057d4d5306947c2fec7ef92fd5b8e2e499d99b560f9f9a837075e43795"
},
"csharp-recursive-pattern/Program.cs": {
"captureGroups": 13,
"digest": "4bb642f82e6de5de638eb9ffb9f8fa79a50e32a8fb5b7f71ee92368b96a350a9"
},
"csharp-return-type/Models/Repo.cs": {
"captureGroups": 7,
"digest": "ab4a56185a33d8afcbcf9a4c60bb8c6c90be611bc9e61516a768a2c67f4616f5"
},
"csharp-return-type/Models/User.cs": {
"captureGroups": 19,
"digest": "5f0eebeef76cfcf10180cd431fc14443de0a7c6d501a55a24ae774a94bca2bb7"
},
"csharp-return-type/Services/App.cs": {
"captureGroups": 15,
"digest": "d3634ba17d4d96216df5fd0d7e967309dd32793636598b2d160fd19a2eea3e25"
},
"csharp-same-arity-cross-file/App.cs": {
"captureGroups": 47,
"digest": "3b66f87310d7dae98a79a6983996abca827e8b78d37e5a7234e26e9520634c5b"
},
"csharp-same-arity-cross-file/DbLookup.cs": {
"captureGroups": 11,
"digest": "77916f4d60865d277c624d1a7ecde55fdb10e6354e5dc1d516c80677628dee3a"
},
"csharp-same-arity-cross-file/Formatter.cs": {
"captureGroups": 11,
"digest": "83c7bbf6774596aa06aa75480a136f442356bf44bf2a048e9f9a561be613d781"
},
"csharp-same-arity-cross-file/ILookup.cs": {
"captureGroups": 7,
"digest": "ee3c7f29ba0638d2917d364fa39cd80220655b4ee0825eda1d2640e7d9e8c500"
},
"csharp-self-this-resolution/src/Models/Repo.cs": {
"captureGroups": 7,
"digest": "03009e02efb0bb1fb33dae6cd15b99c46f2e11a30da79fb8696ec3066c203db0"
},
"csharp-self-this-resolution/src/Models/User.cs": {
"captureGroups": 11,
"digest": "769120fcebccfa2088d22b3a5a1dcabf49ab134fae03688a26b5ca2dafe6aca9"
},
"csharp-spurious-edges-no-csproj/Legacy/System/Threading/Tasks.cs": {
"captureGroups": 7,
"digest": "41d7afa4d3c8cddaaba2dce5c725964a04cc20c7b3d766cbb163ec3e19ff0080"
},
"csharp-spurious-edges-no-csproj/Models/User.cs": {
"captureGroups": 5,
"digest": "b9d373709df7aef537bbdf28cf00d9a71b8ef8a1d217180e60c9c70c511d6c64"
},
"csharp-spurious-edges-no-csproj/Services/OrderService.cs": {
"captureGroups": 14,
"digest": "36e86f9b88e113c1195564ad7d5ded5cbba7250ef0d4b2dfb7187e2fa6b7f9e4"
},
"csharp-spurious-edges/Legacy/Tasks.cs": {
"captureGroups": 7,
"digest": "6c7f8cd5275bb0d9754190762b6381424fbe86d0718739ab7e40757a0c0cd446"
},
"csharp-spurious-edges/Models/User.cs": {
"captureGroups": 5,
"digest": "b9d373709df7aef537bbdf28cf00d9a71b8ef8a1d217180e60c9c70c511d6c64"
},
"csharp-spurious-edges/Services/OrderService.cs": {
"captureGroups": 14,
"digest": "36e86f9b88e113c1195564ad7d5ded5cbba7250ef0d4b2dfb7187e2fa6b7f9e4"
},
"csharp-struct-overloads/src/Calc.cs": {
"captureGroups": 15,
"digest": "fabf22986700fb7bff0521b504e103583dd2b204b1765a3bbd7b5062d310f9a0"
},
"csharp-super-resolution/src/Models/BaseModel.cs": {
"captureGroups": 7,
"digest": "3e5355115690e1b80947138c0bb1ef9f1a3a3073371bed6cfb354e7367c0d46e"
},
"csharp-super-resolution/src/Models/Repo.cs": {
"captureGroups": 7,
"digest": "03009e02efb0bb1fb33dae6cd15b99c46f2e11a30da79fb8696ec3066c203db0"
},
"csharp-super-resolution/src/Models/User.cs": {
"captureGroups": 9,
"digest": "231c0dabbb2e5d105ee19ae3d043f576315f4ec8764baaa310dda95770175827"
},
"csharp-switch-pattern/Models/Repo.cs": {
"captureGroups": 7,
"digest": "b539bad52d6d4ace3120ff489d2524717ba6c9cdd9df8f242e6dc2848a9c8731"
},
"csharp-switch-pattern/Models/User.cs": {
"captureGroups": 7,
"digest": "a16151da571f84e36a531970767568ce684e09bf47a6ec5ce50410e7be675a09"
},
"csharp-switch-pattern/Program.cs": {
"captureGroups": 12,
"digest": "49dbf7471b0bfbf26b37a82ab0794e74cf6ffcc2a04fed5dc3c9df3bece8e38c"
},
"csharp-using-static/App/Calculator.cs": {
"captureGroups": 9,
"digest": "9e6b557a1920d5f793e0bc6c6bbdf4dc9d74d7ff50e7b833961ec089e265551f"
},
"csharp-using-static/Helpers/MathUtils.cs": {
"captureGroups": 6,
"digest": "054eff66bd3582079779542135943e70f9d9f95b03329e45944eb85077290c08"
},
"csharp-var-foreach/Models/Repo.cs": {
"captureGroups": 7,
"digest": "b539bad52d6d4ace3120ff489d2524717ba6c9cdd9df8f242e6dc2848a9c8731"
},
"csharp-var-foreach/Models/User.cs": {
"captureGroups": 7,
"digest": "a16151da571f84e36a531970767568ce684e09bf47a6ec5ce50410e7be675a09"
},
"csharp-var-foreach/Program.cs": {
"captureGroups": 27,
"digest": "9b415bc4739d5ee3e4e538f225a96694391029be69b662de35d9edf47116f345"
},
"csharp-variadic-resolution/Services/App.cs": {
"captureGroups": 9,
"digest": "51b9c158776e89f6be4225849f31470889c442c6e064f5c38bc32806a691106b"
},
"csharp-variadic-resolution/Utils/Logger.cs": {
"captureGroups": 7,
"digest": "abc447c521d792d089fefc63ac281c5af0f94dd76dc284e83296f20617ab6b14"
},
"csharp-write-access/Models.cs": {
"captureGroups": 9,
"digest": "0d65b1d41a42a5a79eecf3a2ff2c7b3585c52dadd479770c2572a146afdc6606"
},
"csharp-write-access/Service.cs": {
"captureGroups": 10,
"digest": "1782eca84697c9ebd7fe802d1e0e0952d29cef8d8834e826b359388561a41c06"
},
"synthetic:dao-20": {
"captureGroups": 222,
"digest": "058e8fd3360af32ce483e6be7887ee12cd1a4150904b43f88d1fa7bd9bb7577b"
}
}

View file

@ -0,0 +1,55 @@
struct B {
void inherited();
};
void sink(int value);
void ambiguous(int value);
void ambiguous(double value);
void helper();
namespace tools {
void namespaceHelper();
}
using tools::namespaceHelper;
template <class... Ts>
void logMany(int, Ts... xs) {
(sink(xs), ...);
}
template <class... Ts>
void foldAmbiguous(Ts... xs) {
(ambiguous(xs), ...);
}
template <class... B>
struct Mix : B... {
void run() {
inherited();
helper();
namespaceHelper();
}
};
template <class T>
struct Current {
void own();
void run() {
own();
}
};
template <class T>
struct UnknownSpecialization {
typename T::value_type value;
void run() {
value.use();
}
};
void callVariadic() {
logMany(1, 2, 3);
}

View file

@ -0,0 +1,554 @@
{
"php-abstract-dispatch/src/Contracts/Repository.php": {
"captureGroups": 10,
"digest": "3459af9360d51aaaba72963fc49bb79edc188e97f9831421d6504c7919b61dc5"
},
"php-abstract-dispatch/src/Repositories/SqlRepository.php": {
"captureGroups": 13,
"digest": "01093dcbb7c4482e93572c59d091c6ece2332d46badd7d7a5b44fd2f7cad5507"
},
"php-abstract-dispatch/src/app.php": {
"captureGroups": 10,
"digest": "52ce761f1d53a56034124fa5e674863bce9092c1b523f6d71139c4f335e8b9f0"
},
"php-alias-imports/app/Models/Repo.php": {
"captureGroups": 14,
"digest": "4fd03e4a19a7a7c2da925f850563b6dccd21b898223c00278bc2753e8841d844"
},
"php-alias-imports/app/Models/User.php": {
"captureGroups": 14,
"digest": "5a20d985b3f1c04e998feb586e8b9b382ab069829db69b156ab2636867096a93"
},
"php-alias-imports/app/Services/Main.php": {
"captureGroups": 15,
"digest": "bf8e1b8079956e7ef9ebfcb3ca38f547e9760da5c83856e93163efe6961067b9"
},
"php-ambiguous/app/Models/Dispatchable.php": {
"captureGroups": 6,
"digest": "d2c8aabf58d4917fd0c9a6ab0a512473a41d449bdd24f7fb5e5cc5e854b5de04"
},
"php-ambiguous/app/Models/Handler.php": {
"captureGroups": 7,
"digest": "0bb9adefa360767db1741b884bf3265b81ad70bce4cd8391d3049763d5282715"
},
"php-ambiguous/app/Other/Dispatchable.php": {
"captureGroups": 6,
"digest": "e1c80e8c21166f4156beb3f22d3bffc7c67dc072b3a9d2f454dd2aacaa845a01"
},
"php-ambiguous/app/Other/Handler.php": {
"captureGroups": 7,
"digest": "f6ad05a50da70b32744792da353d520d63722bca7c0802e603af5eb13a223c5c"
},
"php-ambiguous/app/Services/UserHandler.php": {
"captureGroups": 10,
"digest": "90054792db28ad77054994b483305ebf82d8a8959a0350a721d4a76438aea0a0"
},
"php-app/app/Contracts/Loggable.php": {
"captureGroups": 7,
"digest": "46cf7e385db7656514b796908134a50f4a536f3adda64196e17d8c54f5cd5e3a"
},
"php-app/app/Contracts/Repository.php": {
"captureGroups": 10,
"digest": "40bbdb3fc0ec9b5a9bb8fbdf4a053099ac926951ecd746f2436569add3f0e3d5"
},
"php-app/app/Enums/UserRole.php": {
"captureGroups": 7,
"digest": "aadf56b87d67191b13f4e3f9fae7e8097f4c8a81d13135645ceb8c5be1063a9b"
},
"php-app/app/Models/BaseModel.php": {
"captureGroups": 16,
"digest": "3a768a8443ad599d8501e9aa1e56535cb32ba8b13779b837a68781df3442edff"
},
"php-app/app/Models/User.php": {
"captureGroups": 25,
"digest": "641b8f77fc29b102d2c4d4f04cfea8f5a64f769c0d53da9282c0d9505e1e7dd9"
},
"php-app/app/Services/UserService.php": {
"captureGroups": 37,
"digest": "4c20f536c2df20a9fc1cac33aa48a06535898dcf4f46c62067c8ffd9deab5dbd"
},
"php-app/app/Traits/HasTimestamps.php": {
"captureGroups": 11,
"digest": "d114ed0515e28b83b72ead59ab33114ade0b4fc9ac417ce3bd56acab4046371d"
},
"php-app/app/Traits/SoftDeletes.php": {
"captureGroups": 15,
"digest": "0a697e33e1c98fd092918fb96d72f3fbd0d3c56158c74fe166976fd4f6a5b676"
},
"php-assignment-chain/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "f083497f2ec111e8d396abeb4117e1495035334c25f050959c89aae5d05f76e3"
},
"php-assignment-chain/app/Models/User.php": {
"captureGroups": 7,
"digest": "5e6b0b6f2bbe05e8ee310e5a91163b087b846c38a07dc22ab3ba165b939d7691"
},
"php-assignment-chain/app/Services/AppService.php": {
"captureGroups": 15,
"digest": "36c5358d7f724cc0906e087828c86d9a29fe0629e2bad6105967fe8d671aa6fe"
},
"php-call-result-binding/App.php": {
"captureGroups": 23,
"digest": "48c7af06cd4c12f07f308d8804c1c3652c2af87d155831fc5ee915fd984737d2"
},
"php-calls/app/Services/UserService.php": {
"captureGroups": 7,
"digest": "94f1cdfb0c444dc9e8116d1bbf824dc88584046539b2cbc48297729eb49e41a1"
},
"php-calls/app/Utils/OneArg/log.php": {
"captureGroups": 5,
"digest": "bd3d0b4f7da34edfa86b3e5e9cd6664439baf7cf07318098ac562f48c40f2a72"
},
"php-calls/app/Utils/ZeroArg/log.php": {
"captureGroups": 4,
"digest": "c9c4d8553aece69141049625b2fd36b0b0d4946eacd8eac6b0e2231922e82d48"
},
"php-child-extends-parent/src/App.php": {
"captureGroups": 11,
"digest": "0c8a7c7edaa20009b71f22fef98230119a4738d2a21c8b88d64047c3c932fd36"
},
"php-child-extends-parent/src/Child.php": {
"captureGroups": 4,
"digest": "4943bc0abc98546bb82d98e17260d686805a6aa644aabf2b86ad75dc8334b73b"
},
"php-child-extends-parent/src/Parent.php": {
"captureGroups": 7,
"digest": "e7bbcd72127b97475851b17cf7fbc5a86786091b2117313dfb24deab8a0d6d09"
},
"php-constructor-calls/Models/User.php": {
"captureGroups": 14,
"digest": "a7d4f11e20fb687f537d1ca0735f8e85c9ea8a2b29867bb57326f02da3bb63d1"
},
"php-constructor-calls/app.php": {
"captureGroups": 8,
"digest": "23ef0f05446126892e598872a0b3c3d2f96e3b0dc50e301f1fa784a56ebc81d4"
},
"php-constructor-promotion-fields/Models.php": {
"captureGroups": 22,
"digest": "ac49ef4de6665d63a0bef9449e1c685f2fd20fbb01c5d26c924ee0d469f614c5"
},
"php-constructor-promotion-fields/Service.php": {
"captureGroups": 8,
"digest": "d7093850a8c703a5aefbcd584e5bc8e7ac5668e14ff79e27b1db401f72e83073"
},
"php-constructor-type-inference/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "44921bb2ae5aba27957cd45a7d3e515425f27f8e93af5d6285125b94f5d9ba88"
},
"php-constructor-type-inference/app/Models/User.php": {
"captureGroups": 7,
"digest": "cec22d127d637784baa722787a0a5e5402970ca2dc23a20f07e644762492d8bd"
},
"php-constructor-type-inference/app/Services/AppService.php": {
"captureGroups": 15,
"digest": "26181127fe9e9bf04431a7cc801624bde9dd284049627bb7ca0a4c9234141eec"
},
"php-deep-field-chain/Models.php": {
"captureGroups": 26,
"digest": "f20d780f2bc95ed438047c8e9bb694f5a020454161d8a20f8d5e02be68e4a602"
},
"php-deep-field-chain/Service.php": {
"captureGroups": 9,
"digest": "0026ff9cbad4125b5c27ce356b7c954d369604c5129460d9a0f51cd1c46afbca"
},
"php-default-params/app.php": {
"captureGroups": 9,
"digest": "c5b100802ae7d78869f099ab886133d16f865a653e05bdc477e6b4e523accf91"
},
"php-dynamic-calls/app/Services/Dynamic.php": {
"captureGroups": 52,
"digest": "1a15600d585851ee1f05927c27e6ce6ccaf6a3f56aecf36e27f84a20a65bb9b2"
},
"php-dynamic-calls/app/Services/OtherTargets.php": {
"captureGroups": 6,
"digest": "f27d95bd651a23664a440e8264d793f30ed46dfea915b18300f51448db025528"
},
"php-dynamic-calls/app/Services/Targets.php": {
"captureGroups": 31,
"digest": "7f089e347f1f690400afb890637eb960c70ea5afcb843c534c5d80f650e613a2"
},
"php-field-types/Models.php": {
"captureGroups": 17,
"digest": "23e6855d9ac081c156d7f273a6ca4076c878d900d97822b436d2f190e2272064"
},
"php-field-types/Service.php": {
"captureGroups": 8,
"digest": "d7093850a8c703a5aefbcd584e5bc8e7ac5668e14ff79e27b1db401f72e83073"
},
"php-foreach-call-expr/Repo.php": {
"captureGroups": 17,
"digest": "f6082a43eb279f1de8f8ffea93c1a8ee05b7c5b39fdf2eb0a308c8170b7ab7d1"
},
"php-foreach-call-expr/User.php": {
"captureGroups": 17,
"digest": "799e12a9ebddde34f030e0612724f8a31893d33d45421607e619070036e30937"
},
"php-foreach-call-expr/main.php": {
"captureGroups": 11,
"digest": "ce332b01f1bbdfaf0e03a3d77f7f708e9f7e5fd41eaa795be8fd2584ab86711a"
},
"php-foreach-generic/App.php": {
"captureGroups": 16,
"digest": "6f5355bc0d315224e3ce2696f3fac3b067f90771f291ce73e2cfc9c5c9ece59c"
},
"php-foreach-generic/Repo.php": {
"captureGroups": 13,
"digest": "0ba7bc5bc2387e1f07898a288a4d856349b777c9be12ace6ebd2ccf39124c446"
},
"php-foreach-generic/User.php": {
"captureGroups": 13,
"digest": "eb34a7e89368d13116de3cb7b0430bc2d1b9c982e3b3f7c9511dae8a73759af6"
},
"php-foreach-loop/App.php": {
"captureGroups": 10,
"digest": "0a648289cb93a52808d1d6ee8b519fa28c413227607deb0c3d3a9a0cdaa52e02"
},
"php-foreach-loop/Repo.php": {
"captureGroups": 13,
"digest": "0ba7bc5bc2387e1f07898a288a4d856349b777c9be12ace6ebd2ccf39124c446"
},
"php-foreach-loop/User.php": {
"captureGroups": 13,
"digest": "eb34a7e89368d13116de3cb7b0430bc2d1b9c982e3b3f7c9511dae8a73759af6"
},
"php-foreach-member-access/App.php": {
"captureGroups": 14,
"digest": "44e6f810de40d566b5c909d91dcc50de5282fde2f597bf38302edbcb8d825ca3"
},
"php-foreach-member-access/Repo.php": {
"captureGroups": 13,
"digest": "0ba7bc5bc2387e1f07898a288a4d856349b777c9be12ace6ebd2ccf39124c446"
},
"php-foreach-member-access/User.php": {
"captureGroups": 13,
"digest": "eb34a7e89368d13116de3cb7b0430bc2d1b9c982e3b3f7c9511dae8a73759af6"
},
"php-fqn-cross-namespace/app/Models/User.php": {
"captureGroups": 7,
"digest": "56f9e84d179973e9700a0768c291415a7e75795443ebc2940c32bf6665761430"
},
"php-fqn-cross-namespace/app/Other/User.php": {
"captureGroups": 7,
"digest": "9989dd5b86b705df941fc63dfd7fcbe4520dd1378a16a50b5f3dfd0ebed7bd90"
},
"php-fqn-cross-namespace/app/Services/Service.php": {
"captureGroups": 15,
"digest": "94a2bec57ccde7aa663ce2027c7f36151d18ee257830663365bc14f9d4d5f703"
},
"php-grandparent-resolution/app/Models/A.php": {
"captureGroups": 9,
"digest": "47af1c30a957f15a2a5ebbddd80c0214a4f68c15f08699fc81cdbfaefa4a70b5"
},
"php-grandparent-resolution/app/Models/B.php": {
"captureGroups": 4,
"digest": "d6326a8eb65bfa8da20d9fb0e5ce07c61eeabd5d53cc94faaebbc3dda0274359"
},
"php-grandparent-resolution/app/Models/C.php": {
"captureGroups": 4,
"digest": "92fb1dabd5e6dc6eb4115d26303c96ba3d5e34ef9d0ccd965f531b5cf34b7390"
},
"php-grandparent-resolution/app/Models/Greeting.php": {
"captureGroups": 7,
"digest": "dd6504594d3d997d93dd44a0de354f4c7a9d564613596ef2da2e13eef7b012ed"
},
"php-grandparent-resolution/app/Services/App.php": {
"captureGroups": 12,
"digest": "5d0c3524e1ca41ee57bd05fd0a2831804598fca8079ea4ece38045d9c4bb1113"
},
"php-grouped-imports/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "82b2bc7323990945f9f3c122812d1ada5cf12d7f2b2e829dbb5e9366bb2b3602"
},
"php-grouped-imports/app/Models/User.php": {
"captureGroups": 7,
"digest": "4028b426d00caf369e42e1fbfaa1a11440a598fe15734d2eb03cf017ae5f0a1f"
},
"php-grouped-imports/app/Services/Main.php": {
"captureGroups": 15,
"digest": "65ea5a0c2fd2b523ab1bf2e3f632bd3debeb7613c88496362016f9e590b64972"
},
"php-local-shadow/app/Services/Main.php": {
"captureGroups": 9,
"digest": "b4b3e35399501d98521dc9d9281a4e5c94449b580cc6cb9b3ed4187e79ee2c07"
},
"php-local-shadow/app/Utils/Logger.php": {
"captureGroups": 5,
"digest": "7434c0a1b2282daf8d60334a85523d603bffd256eaaf5c241509e72c0a6d1e5d"
},
"php-member-calls/app/Models/User.php": {
"captureGroups": 7,
"digest": "cec22d127d637784baa722787a0a5e5402970ca2dc23a20f07e644762492d8bd"
},
"php-member-calls/app/Services/UserService.php": {
"captureGroups": 11,
"digest": "6f6d5d34edd1cd4e32ea77db7e4b07b73de9ca09bb658123455820af8228d0cc"
},
"php-method-chain-binding/App.php": {
"captureGroups": 48,
"digest": "8cab0917ecbbd65f43046dcc3c03f7af2d8e1a51780d4dbf3f7bc29ccabd2fa2"
},
"php-method-enrichment/src/Models/Animal.php": {
"captureGroups": 12,
"digest": "f32de8a30558f2678e73c68d7db9c6eab827fd8d5f7655b906a76066c8421bef"
},
"php-method-enrichment/src/Models/Dog.php": {
"captureGroups": 8,
"digest": "ffe9909633e018ae077b685a83d18dde30d30b818068b7ef6674cbd184ebdf33"
},
"php-method-enrichment/src/app.php": {
"captureGroups": 11,
"digest": "52791f6945c8c4ae083b816bd3af239bce44f0e97cbf80cdc119f4f366015138"
},
"php-mro-arity-mismatch/app/Models/ChildModel.php": {
"captureGroups": 15,
"digest": "e007097393563883393a2469befbbe76568fa25dce87d15cc808a877edc0177c"
},
"php-mro-arity-mismatch/app/Models/Orphan.php": {
"captureGroups": 9,
"digest": "29d2e5732698a9951194a6be3d788e337a142c5ea174c5356d375cf9c9e9ff58"
},
"php-mro-arity-mismatch/app/Models/ParentModel.php": {
"captureGroups": 12,
"digest": "e16afaad3145133dce0f2de89d0f58eec33492b6a67741013eaf27e35ce6aa07"
},
"php-mro-arity-mismatch/app/Services/Caller.php": {
"captureGroups": 22,
"digest": "d51fdbcf226f31f9220a506cceb9f538642ff99d238a1828b6b43fce6193f664"
},
"php-namespace-fallback-isolation/src/App/Caller.php": {
"captureGroups": 13,
"digest": "d5040d068fd8227388da7f25cc471f154adb37cd6bbfb78b5cac00f44bf45734"
},
"php-namespace-fallback-isolation/src/App/Utils/Caller.php": {
"captureGroups": 8,
"digest": "7f7a357fb5d758f9d4686b072c4d5df82026701ed28bdce042c5c7950a893c41"
},
"php-namespace-fallback-isolation/src/App/Utils/Format.php": {
"captureGroups": 5,
"digest": "dc33b55ffcef13d75972ca695921ce75c88ea21f37960ad138ee04267f3ccba4"
},
"php-namespace-fallback-isolation/src/Vendor/Utils/Format.php": {
"captureGroups": 7,
"digest": "eff9d04520000bfc5fd7ae87721965e3000d1b478786f95b4f51e001ad877faf"
},
"php-nullable-receiver/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "44921bb2ae5aba27957cd45a7d3e515425f27f8e93af5d6285125b94f5d9ba88"
},
"php-nullable-receiver/app/Models/User.php": {
"captureGroups": 7,
"digest": "cec22d127d637784baa722787a0a5e5402970ca2dc23a20f07e644762492d8bd"
},
"php-nullable-receiver/app/Services/AppService.php": {
"captureGroups": 13,
"digest": "cea6ae3f9e32a3e0448a279034bcf039b1d5af6334ab1b1ae43d9c55b6ca094d"
},
"php-overload-dispatch/src/Services/Formatter.php": {
"captureGroups": 7,
"digest": "5a4979e230bd666c0dc35e8e9611adf67d770858d1897ea113610f0698ccea7d"
},
"php-overload-dispatch/src/Services/FormatterExtended.php": {
"captureGroups": 9,
"digest": "17dadd44959529ba4533df45561025d00be7ae2f0a2503cbde4a69e9b72614c1"
},
"php-overload-dispatch/src/app.php": {
"captureGroups": 10,
"digest": "d11621fbaec9f5015e61f35b5c2747b9f2da090a346adc5fcc10f191af61f048"
},
"php-parent-resolution/app/Models/BaseModel.php": {
"captureGroups": 7,
"digest": "6c2081e07e8b2ea2492acfe6dcacb99505faf92749f9c570636434abda9625a1"
},
"php-parent-resolution/app/Models/Serializable.php": {
"captureGroups": 6,
"digest": "e047de645ae2b8029a4bb31a61bbc556f58da066d346a2e57101d22cb8dcbb6a"
},
"php-parent-resolution/app/Models/User.php": {
"captureGroups": 8,
"digest": "b34ddf79f02bc1f8a233c91e0d7ad7471cc50912897d619b03c9c60e111de2e6"
},
"php-parent-vs-trait/app/Auditable.php": {
"captureGroups": 7,
"digest": "e11a6f4e2e3eaac87cf26fe038d370672525ab4d2925125e9494eb0e275cc19c"
},
"php-parent-vs-trait/app/Base.php": {
"captureGroups": 7,
"digest": "d7f05cb3f8cf09740fd7063932cc4fccb5b6fff553093b64a341e2503ec42f2d"
},
"php-parent-vs-trait/app/Child.php": {
"captureGroups": 14,
"digest": "8b0149c1d5d54d5d2cb743bbf43d424bc19cf758f4dde04105084c0903117825"
},
"php-phpdoc-attribute-return-type/Models.php": {
"captureGroups": 11,
"digest": "8c5cd8fb19d4f3964c68533834db49d85eb5f93e04ba067a617f5a6c655a0046"
},
"php-phpdoc-attribute-return-type/Services.php": {
"captureGroups": 37,
"digest": "d857f9bc514191f222a2e0880c67ab958f2689fc1c6ac0a55af9c8e26fd9c0c0"
},
"php-phpdoc-return-type/Models.php": {
"captureGroups": 11,
"digest": "8c5cd8fb19d4f3964c68533834db49d85eb5f93e04ba067a617f5a6c655a0046"
},
"php-phpdoc-return-type/Services.php": {
"captureGroups": 37,
"digest": "17ba0088f086c768d5ab6a07de513669a41787ca5f26c33114e6647ef683c920"
},
"php-property-promotion/UserService.php": {
"captureGroups": 19,
"digest": "872830776a740cf97c1eafac5e3413aae99e0e951b1b5b46d1aa0df821eac8bc"
},
"php-receiver-resolution/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "44921bb2ae5aba27957cd45a7d3e515425f27f8e93af5d6285125b94f5d9ba88"
},
"php-receiver-resolution/app/Models/User.php": {
"captureGroups": 7,
"digest": "cec22d127d637784baa722787a0a5e5402970ca2dc23a20f07e644762492d8bd"
},
"php-receiver-resolution/app/Services/AppService.php": {
"captureGroups": 13,
"digest": "59783c7af75e9075f00f425984ca1ba7a4c556bb0301e2eb2c3fdcaa94cd547f"
},
"php-response-shapes/api/items.php": {
"captureGroups": 11,
"digest": "84e64331d581f16103b3b0c8c95819e41c36ca2e16b6186ccfa73a13729577a1"
},
"php-response-shapes/api/submit.php": {
"captureGroups": 17,
"digest": "f5d1af2ad313f4639fbad558eed65d3fa0c218eefc55843ea9e07156a1554883"
},
"php-response-shapes/includes/auth.php": {
"captureGroups": 4,
"digest": "7734a9fee1a2a34df7a8857bda37e0930affce7966165023e76c83a71c9f214f"
},
"php-return-type/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "f04e8cb824fba439cea5fdab461ade906b49953b7e8cd2777e724f8fa66e9b31"
},
"php-return-type/app/Models/User.php": {
"captureGroups": 14,
"digest": "5b41d9519e2d696436cb5e6a1337619def5882c2bc442af74ac15a8570b6e168"
},
"php-return-type/app/Services/UserService.php": {
"captureGroups": 17,
"digest": "fcc2d78ac4bdde1ac5179e6623a35299465bcbf9bb016375100bcb636624d043"
},
"php-self-this-resolution/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "70b05bbb0ad6e8c1ba62d3355dbc1ffa6ac01c9e85ac22e43a07e387eddf401e"
},
"php-self-this-resolution/app/Models/User.php": {
"captureGroups": 11,
"digest": "86fea63dd23c57956769db89241351af6a86acc69e9c226a8ee859ab531b18ca"
},
"php-super-resolution/app/Models/BaseModel.php": {
"captureGroups": 7,
"digest": "6c2081e07e8b2ea2492acfe6dcacb99505faf92749f9c570636434abda9625a1"
},
"php-super-resolution/app/Models/Repo.php": {
"captureGroups": 7,
"digest": "70b05bbb0ad6e8c1ba62d3355dbc1ffa6ac01c9e85ac22e43a07e387eddf401e"
},
"php-super-resolution/app/Models/User.php": {
"captureGroups": 9,
"digest": "e62c49645b1ef609bb915c355b06379d996c030c3eaacbe40ca49d0d0b323e64"
},
"php-this-receiver-disambiguation/AdminService.php": {
"captureGroups": 15,
"digest": "db9ba7004ba7b98984307f2b3e1a71154b5a7d60403286d7f308c03ed351b18a"
},
"php-this-receiver-disambiguation/Models.php": {
"captureGroups": 11,
"digest": "8c5cd8fb19d4f3964c68533834db49d85eb5f93e04ba067a617f5a6c655a0046"
},
"php-this-receiver-disambiguation/UserService.php": {
"captureGroups": 15,
"digest": "147924d3638edc63eef1a009943fae66a87ac85d02ae533428646c644e81c2bb"
},
"php-transitive-traits/app/Models/Consumer.php": {
"captureGroups": 17,
"digest": "a447ed654cc0e074c2f0d93b0d371953e823c9a77da559e841741f5b73b97058"
},
"php-transitive-traits/app/Traits/TraitA.php": {
"captureGroups": 7,
"digest": "53ca5a3b0407c54a2d6ce80a83eee592c32443795a01186bc9c4cac680e050b7"
},
"php-transitive-traits/app/Traits/TraitB.php": {
"captureGroups": 7,
"digest": "abafc915f22428f94e82ca1450902b6ef0599557e05aea345f388392b8097bf2"
},
"php-transitive-traits/app/Traits/TraitC.php": {
"captureGroups": 7,
"digest": "9d55a120a79ead7605f2e05c80b36615c228acd5cf83db5417a5efe4c9ed9e7f"
},
"php-typed-properties/app/Models/UserRepo.php": {
"captureGroups": 11,
"digest": "19089a297be5f83a4a7610a2111a1fb1b84806cda5fa7d6dbb16c9dfc137b6d6"
},
"php-typed-properties/app/Services/UserService.php": {
"captureGroups": 12,
"digest": "2ec84482a3e2332f3f15ebb3f2d8d01d1d56e62da256127ece571a8c2e0c070c"
},
"php-typed-property-dedup/app/Models/UserRepo.php": {
"captureGroups": 7,
"digest": "4b0043ea779b33cabc12f79cb4aef32759284137f20838346e9f5ef82076be7b"
},
"php-typed-property-dedup/app/Services/Mixed.php": {
"captureGroups": 14,
"digest": "9f4ba6bd183c20acaad547b6a713e80498eb70eaabaa7b416650bb64098bde0d"
},
"php-unresolved-receiver-arity/app/Models/Handler.php": {
"captureGroups": 21,
"digest": "0e45194dc4a1bf420d4325756a780b0574129517e70d83a82c5dd68b921a0f67"
},
"php-unresolved-receiver-arity/app/Services/Caller.php": {
"captureGroups": 28,
"digest": "a3d316585e67b65ac85a63991735c1324911c709a6d9714f9c3eb4ca6a95f807"
},
"php-use-function-const/app/Config/constants.php": {
"captureGroups": 2,
"digest": "c045ce0a40c3396cdfbbf0fbbe24792fe2bba6dfba14f739b15a4bb5ae90a778"
},
"php-use-function-const/app/Models/User.php": {
"captureGroups": 10,
"digest": "ace0bf7cac67791143d870792b7b870ecc04730bbd98249d8113dc246e90113f"
},
"php-use-function-const/app/Services/Calculator.php": {
"captureGroups": 15,
"digest": "d6996da68f917c3ae6b52b530d166e8266f5053ce6c9b76e1643cb61edcafa38"
},
"php-use-function-const/app/Utils/helpers.php": {
"captureGroups": 6,
"digest": "98ebf5b3427b39b619e78eaf11a46376eba5ce7fef202508ce169afd8c8e979e"
},
"php-variadic-arity-minimum/app/Services/Caller.php": {
"captureGroups": 29,
"digest": "d5669fe609abae6b7db19c15c0cb795859e0b4b0570ae83fbe80a392f3775ca8"
},
"php-variadic-arity-minimum/app/Utils/Logger.php": {
"captureGroups": 18,
"digest": "dbcc7f0bf265a950952c4d192bef14924b29b31326942f4f1600aa8bbe22e9a9"
},
"php-variadic-resolution/app/Services/AppService.php": {
"captureGroups": 9,
"digest": "8b5298358fba8f578b2470f9d93ffbc1089d1ef3208c1142185880b4dc174751"
},
"php-variadic-resolution/app/Utils/Logger.php": {
"captureGroups": 7,
"digest": "8e075546630b3057577932dcf8a876b450950a5accd7df3f8a87079cdc401139"
},
"php-write-access/models.php": {
"captureGroups": 13,
"digest": "b7f00a2f5e9c64d9823d11279d831bf9320c697e69184f368d288348fc9ac39e"
},
"php-write-access/service.php": {
"captureGroups": 8,
"digest": "5ef6b1687663e4cd9acb880a570fc32388961e735ad38d8ea9906da173971362"
},
"synthetic:dao-20": {
"captureGroups": 221,
"digest": "e29ee5132bb30bf939508cf3741159856b78881c01447acd290e18b1c5121225"
}
}

View file

@ -0,0 +1,758 @@
{
"python-abstract-dispatch/app.py": {
"captureGroups": 11,
"digest": "8662c17b0f21fcfa650065abd62f0c9b7e1c65bf8a1f7dce6f2a16bba9df759f"
},
"python-abstract-dispatch/base.py": {
"captureGroups": 15,
"digest": "9c2891a6143c10cc3d81603c8623e54b6914cd8aaa668e84b59c1602492c0e4b"
},
"python-abstract-dispatch/impl.py": {
"captureGroups": 14,
"digest": "2a7ec28b431cb4010829bca1f0cadf3b11ef7a67abe16f322fc4329a5b619223"
},
"python-alias-imports/app.py": {
"captureGroups": 13,
"digest": "ae1dc39a6d300a38bade534e58a1fcf5141eee06c6e903645a80dbce175b4949"
},
"python-alias-imports/models.py": {
"captureGroups": 11,
"digest": "abf5fcdf7cc473efa5a149319b0fc3d14b3ae91a2e9a14d621243c5ed7fafafa"
},
"python-ambiguous/models/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-ambiguous/models/handler.py": {
"captureGroups": 6,
"digest": "a148084ab160b53bd9a59f12a605447dee85eca1a46abfcca62fc48de56ca3a3"
},
"python-ambiguous/other/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-ambiguous/other/handler.py": {
"captureGroups": 6,
"digest": "41330f66b51bcedb14cdcd4cbb88601e3426e246d547ab7cf060da7c294bc8de"
},
"python-ambiguous/services/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-ambiguous/services/user_handler.py": {
"captureGroups": 7,
"digest": "2a441d1b24e272fdf5c214401d59211b6dff18b56bdaa06148f633ee3a5105e2"
},
"python-ancestor-import/a/b/c/deep.py": {
"captureGroups": 5,
"digest": "633dd357999ff9124b5c1faba69d0156f4a996693f4cb5dcd05e6cf530d897ea"
},
"python-ancestor-import/a/utils.py": {
"captureGroups": 3,
"digest": "4d85820bac0e64fb70eceee1454424a75bc3db10f9fc9e7d822e3539ef50c8c1"
},
"python-ancestor-import/backend/middleware.py": {
"captureGroups": 10,
"digest": "ed9769cc43e5e1b6dcaa5179e1c80f07ef15e75e728e6ae039091454b966fcec"
},
"python-ancestor-import/backend/services/auth.py": {
"captureGroups": 7,
"digest": "73968a5d120b0a97e95e96b7c0c7433575e8178a832b735b42d5be34c72e2ba3"
},
"python-assignment-chain/app.py": {
"captureGroups": 27,
"digest": "26ab1b7aacc902373062b7d4a5c88e41619a3352664cf27e3d3cd92012f6a24a"
},
"python-assignment-chain/repo.py": {
"captureGroups": 6,
"digest": "0d2704cfa14e431395973cc82b582f9fae43a8ce67d5548ab5df62b3fa8d1b7e"
},
"python-assignment-chain/user.py": {
"captureGroups": 6,
"digest": "4dd0a9d0f5797bae1c281a07145ddcc4162eae1ea16208e85edb74cd98e085f9"
},
"python-bare-import/models/user.py": {
"captureGroups": 6,
"digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2"
},
"python-bare-import/services/auth.py": {
"captureGroups": 8,
"digest": "8b3659bfdc60eedb1a33fd803f5ad23f1458f134884ed600e3fb4a662e19dbf3"
},
"python-bare-import/services/user.py": {
"captureGroups": 6,
"digest": "0662b53fd75f4892afa665d4dfe51a8dc963971696a370a8b2c4ba124eeda2af"
},
"python-call-result-binding/app.py": {
"captureGroups": 8,
"digest": "338c3922981604e71ddfc60ad61eba4b17f68ca654644e01add942c729b422cf"
},
"python-call-result-binding/models.py": {
"captureGroups": 12,
"digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69"
},
"python-call-result-binding/service.py": {
"captureGroups": 7,
"digest": "81f8d5783bfc95a9f83f02abd15f7bbc0869aaca964c62b40653cc05ab73ebcb"
},
"python-calls/one.py": {
"captureGroups": 3,
"digest": "4eee8157b23b84308163ffc2baa55c56aee3bd97c06365fe9b045f3c5066ad56"
},
"python-calls/service.py": {
"captureGroups": 6,
"digest": "3d469b47150af229a8deb3de8564ec89ad8529fb19a8fd75588160b2898df38f"
},
"python-calls/zero.py": {
"captureGroups": 3,
"digest": "49330572d87d1cbfa7db11351b18734bf23fecd686b46e55a788e6b82eca306f"
},
"python-chain-call/app.py": {
"captureGroups": 9,
"digest": "abc26d2f53c4dc0f091009b120e7869ca74b015bad637c5643fc4fdd69df946e"
},
"python-chain-call/models/repo.py": {
"captureGroups": 6,
"digest": "54e050e7cecc288e367e97dc59280fd4febb614c7099894591e122901261a130"
},
"python-chain-call/models/user.py": {
"captureGroups": 6,
"digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2"
},
"python-chain-call/service.py": {
"captureGroups": 9,
"digest": "250f907deb4fec54761559db26f0938dc0accee480021cf95bcdd5167d33096f"
},
"python-child-extends-parent/app.py": {
"captureGroups": 9,
"digest": "0f60d5cd521b0073524b0993e82d5291f86badd5cbefb986cefdf7b0bed64157"
},
"python-child-extends-parent/child.py": {
"captureGroups": 4,
"digest": "c85d867b0b206cd3e13e8dd94c83b49f13e0f2ddde7acd61785b9755f41d3d14"
},
"python-child-extends-parent/parent.py": {
"captureGroups": 7,
"digest": "2f30a106769eeda283491fee883b448cd4bbcb5a8be2a587e7c72e2dca2a03f1"
},
"python-class-annotations/repo.py": {
"captureGroups": 8,
"digest": "f53cefad4e2dd9951fe34eea13822f5c521d9a23c3185b559b390ada7c5e1f3d"
},
"python-class-annotations/service.py": {
"captureGroups": 11,
"digest": "0b20f98ba8b035c4e065bcfa66df9c955ba98608ea7aece8b998cc62758e5d85"
},
"python-class-annotations/user.py": {
"captureGroups": 8,
"digest": "38e7b4fe8372de734b28227c926d0bc80f66d2539b7f8d0cc22c1d32bf69821b"
},
"python-class-attr-export-leak/app.py": {
"captureGroups": 9,
"digest": "e958f924b72ac35d92cb3a959c4bea866fa1020347e96a09e07b7087ce9a231c"
},
"python-class-attr-export-leak/mod.py": {
"captureGroups": 11,
"digest": "cf175a5a7b577bbf8e76acc004c5360f496d0a10cc5de0a3e31a212bd413b491"
},
"python-class-body-namespace-import/app.py": {
"captureGroups": 9,
"digest": "58843947b979524df15f256d240b789ac67356499013b9c96bcc9e14108f8eff"
},
"python-class-body-namespace-import/mod.py": {
"captureGroups": 4,
"digest": "d6ead25daf8f609f1ea2ce79ccf1a88ad3c0ee2d0c9ced0ab13e8f1e729eecb5"
},
"python-constructor-calls/app.py": {
"captureGroups": 8,
"digest": "e8621bf951ab14634a8ed38a5953ff59434d3406d59e9c16751b6ab9cc40e7e9"
},
"python-constructor-calls/models.py": {
"captureGroups": 11,
"digest": "3508b40afee5ce512fd9057be671629b8a122c88342ab358cbf69ac418a39e34"
},
"python-constructor-type-inference/models/repo.py": {
"captureGroups": 12,
"digest": "a076029a7a594b3577cbe3017c60cbd8f376ae80bd1e5216b0469b165f1c9f76"
},
"python-constructor-type-inference/models/user.py": {
"captureGroups": 12,
"digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69"
},
"python-constructor-type-inference/services/app.py": {
"captureGroups": 13,
"digest": "5306b0960c7b88572e81aa5b203f7e10adbd5d6c8b4c54fc78314624d5864836"
},
"python-default-params/app.py": {
"captureGroups": 17,
"digest": "408a601fac98bb34b153210a3513e97a5d6d0537f5b02d01fffb4525d24395a3"
},
"python-dict-items-loop/app.py": {
"captureGroups": 8,
"digest": "98486424c256def8b89730644deb829ac1c1fc5c46d8f86e586ed2d8ab1a07b8"
},
"python-dict-items-loop/repo.py": {
"captureGroups": 11,
"digest": "a1f088dbd281662c7b0ba9797d07edaef8a2e5a82a6715242eb3b70c29db86aa"
},
"python-dict-items-loop/user.py": {
"captureGroups": 11,
"digest": "e9398cffc0a70490a3b5f90e3436292e306df36ee51955741d8ee91aa37b2e73"
},
"python-django-app-imports/accounts/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/accounts/admin.py": {
"captureGroups": 2,
"digest": "392b15be747e2b5cbd3ac5a9e61a7677ffa6ba52e49d3631681e43c557373b5f"
},
"python-django-app-imports/accounts/apps.py": {
"captureGroups": 5,
"digest": "6fc1529373f9e3183ccd1c9a7013fa2762ef30c36fea7a7afb4ba418aafab9b0"
},
"python-django-app-imports/accounts/migrations/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/accounts/models.py": {
"captureGroups": 7,
"digest": "35025fe0635eb78a60be6ec7a808aa1db586f49a878f16263307b3a04452b4d5"
},
"python-django-app-imports/accounts/tests.py": {
"captureGroups": 2,
"digest": "f43159c7b31ee859e91c768bb7a91b541d6cdfc1fec90e668db6443f11ebcc67"
},
"python-django-app-imports/accounts/views.py": {
"captureGroups": 2,
"digest": "58727216fa6b809839192a0af04b1471e34d9608d9d70c19d4ac6468e9c0529d"
},
"python-django-app-imports/billing/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/billing/admin.py": {
"captureGroups": 2,
"digest": "392b15be747e2b5cbd3ac5a9e61a7677ffa6ba52e49d3631681e43c557373b5f"
},
"python-django-app-imports/billing/apps.py": {
"captureGroups": 5,
"digest": "b93de8fe8c56a8ef878b665667fc5fcfa73b526d095228ed4c972aeff9c1ad44"
},
"python-django-app-imports/billing/migrations/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/billing/models.py": {
"captureGroups": 11,
"digest": "9e7f426103b6c5f53630bddf7c50253bee22c0c018ef85437fdefe0117998053"
},
"python-django-app-imports/billing/tests.py": {
"captureGroups": 2,
"digest": "f43159c7b31ee859e91c768bb7a91b541d6cdfc1fec90e668db6443f11ebcc67"
},
"python-django-app-imports/billing/views.py": {
"captureGroups": 2,
"digest": "58727216fa6b809839192a0af04b1471e34d9608d9d70c19d4ac6468e9c0529d"
},
"python-django-app-imports/config/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/config/asgi.py": {
"captureGroups": 7,
"digest": "79729019cda1dd6f41848365b98416fc610aaa846464bed569573c1fde0892a3"
},
"python-django-app-imports/config/settings.py": {
"captureGroups": 20,
"digest": "776f9193ac3a1927c8b0900efceb5ac3f16400ddfa7fa5d9617153d0bcf6ea0f"
},
"python-django-app-imports/config/urls.py": {
"captureGroups": 5,
"digest": "fb7fd5c6bac541e180e7f90bed248eeac937bc0cec7c2d8405f7f9bb20e87840"
},
"python-django-app-imports/config/wsgi.py": {
"captureGroups": 7,
"digest": "2c57f45963ef158d2ad6a504c5d09fb4f3aa53d6198b91e91f8f824aae1de452"
},
"python-django-app-imports/manage.py": {
"captureGroups": 10,
"digest": "ba770ed72ce287e7739d1b3ec4165053bfd8aa49f9104682d1a7094c724e3039"
},
"python-enumerate-loop/app.py": {
"captureGroups": 23,
"digest": "1f528f2084d1f6da954ec8ce7eef0e45f99a0b3ae58502adc262a6caee5aa661"
},
"python-enumerate-loop/repo.py": {
"captureGroups": 7,
"digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215"
},
"python-enumerate-loop/user.py": {
"captureGroups": 12,
"digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69"
},
"python-field-type-disambig/address.py": {
"captureGroups": 8,
"digest": "03076501a92927ff778f3dbcb555aa59e40fc21b1fe367147fd50cc330637a92"
},
"python-field-type-disambig/service.py": {
"captureGroups": 6,
"digest": "caabe7eb61e0393151c362f3ec18a38b1d23a77176c67814c41ed6a2cfa54005"
},
"python-field-type-disambig/user.py": {
"captureGroups": 11,
"digest": "d71983e74eb3c6ea73b4c8aa6457a6e6d6383538d5e5757aea9fcf18bcbe560b"
},
"python-field-types/models.py": {
"captureGroups": 18,
"digest": "d1cf0a8e91042726967b6671ea720f2ddb57c5cfeb917cd942096ed0cb8bb0e8"
},
"python-field-types/service.py": {
"captureGroups": 6,
"digest": "ad1c6a054f610dc609f3ec963746d49be87567d1bec643c811563bdcedc4469d"
},
"python-for-call-expr/main.py": {
"captureGroups": 15,
"digest": "5c290b3b34f3f5e9dcdd6ee3ae72ba4223337cd64592330f9a8c6c6f13b2fd2d"
},
"python-for-call-expr/models.py": {
"captureGroups": 31,
"digest": "f809317e086279962b81038b7ef2053bd3bc96d35bb1dd84a1ad543bc8dfe068"
},
"python-function-local-import-chain/app.py": {
"captureGroups": 9,
"digest": "ad4d45976ca10c3fc3b7bf498ee23397797368a5d2817311d0e95e943af89199"
},
"python-function-local-import-chain/svc.py": {
"captureGroups": 11,
"digest": "955591b75260b260c00d4ab4918c16ac344d23b5fffa9c47fbbacdc56c67ae47"
},
"python-function-local-namespace-import/app.py": {
"captureGroups": 9,
"digest": "45f477cfd7b4991a8ce04239fde92b60b6c015922ac7cc52d5aa42caf5b69c3b"
},
"python-function-local-namespace-import/svc.py": {
"captureGroups": 4,
"digest": "efe8b8af2d4e7f547a659aecc00399091af2597b47adc7b296bddb10cab2da69"
},
"python-grandparent-resolution/app.py": {
"captureGroups": 9,
"digest": "509f6937b263ae9eb420bdf916664af3f0a0cf4fce708bb912f259b56e7c0938"
},
"python-grandparent-resolution/models/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-grandparent-resolution/models/a.py": {
"captureGroups": 9,
"digest": "5879a42d6655248623c9aee193fedcae51daa1887d8e597fdd944ad93af053a4"
},
"python-grandparent-resolution/models/b.py": {
"captureGroups": 4,
"digest": "55d7ad9e04c57cffa63b0bbcd71bfe18277b6369e7f4facb0faf802eea94db91"
},
"python-grandparent-resolution/models/c.py": {
"captureGroups": 4,
"digest": "1fe0d62c3f332a954e7a7b18dd3c1a6d8d051ecf007d36cb6ecb32d3f7592c38"
},
"python-grandparent-resolution/models/greeting.py": {
"captureGroups": 7,
"digest": "90a9349f89eb7e7c76123c6b235d510a9f151c9cd7e0db07bd8ae9b99f38ee93"
},
"python-local-shadow/app.py": {
"captureGroups": 8,
"digest": "b71ed90838b5898310a80843486871ebeb6e1f63d24e0565d4be947c7bb66dc4"
},
"python-local-shadow/utils.py": {
"captureGroups": 4,
"digest": "c11f7883ea696f30d188f8b8316cfd28746a10b4c7b898807ae21c67a8644ee5"
},
"python-match-case/app.py": {
"captureGroups": 7,
"digest": "9c1f82b03516fbb9e111f1abcc56fd329bbfe290d3e9c0f31e9c8a84d715a031"
},
"python-match-case/models/repo.py": {
"captureGroups": 6,
"digest": "54e050e7cecc288e367e97dc59280fd4febb614c7099894591e122901261a130"
},
"python-match-case/models/user.py": {
"captureGroups": 6,
"digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2"
},
"python-mcp-tools/helper1.py": {
"captureGroups": 3,
"digest": "7110e59dc2f17eb91a3055a133ec9243e706df3ec81be7b16b5f5eed1ae5412f"
},
"python-mcp-tools/helper10.py": {
"captureGroups": 3,
"digest": "78645d6df40eea5a5fe93c795028e71c947632da816b2a0df7ba09c3ec87b209"
},
"python-mcp-tools/helper11.py": {
"captureGroups": 3,
"digest": "6829e8ce4d64652d636f60771534460a8f110df3bbb09ddf1ec3ffbff892bc3f"
},
"python-mcp-tools/helper12.py": {
"captureGroups": 3,
"digest": "b02a4035ee0492ab6df26386bc9d60f508b07bb655ab5a598986380940615d1c"
},
"python-mcp-tools/helper13.py": {
"captureGroups": 3,
"digest": "79464a367f1f1a5819f6b7bc0be14c4bb36f27e9197d71bdd4b541fb0831738e"
},
"python-mcp-tools/helper14.py": {
"captureGroups": 3,
"digest": "ddbb20f3e0c3fdfff5e97b946ca32c761f584534b5dc7ce74c0fd9fef8e0f1e3"
},
"python-mcp-tools/helper2.py": {
"captureGroups": 3,
"digest": "0936e364c53dd42c0bb0d2be02d4432a45b1570e8cf7b6c422aeb2952be4e816"
},
"python-mcp-tools/helper3.py": {
"captureGroups": 3,
"digest": "72a155ec8c5318ba679c51355b1760abbd927356b2503c19aa78a4af645013d1"
},
"python-mcp-tools/helper4.py": {
"captureGroups": 3,
"digest": "9b253f8b946df5dd072c61cca4985c791de90fcd26b1749ccde0eae6a6cbedd6"
},
"python-mcp-tools/helper5.py": {
"captureGroups": 3,
"digest": "e20e5fe81bd196ce552040f22e145dad1a784fcd3fe2cdd374939803394b65ce"
},
"python-mcp-tools/helper6.py": {
"captureGroups": 3,
"digest": "a5f7af11d4c665e802ed1fc9fdbf6ecb8fafcb140067cb1a90f5c1b4c88e4abb"
},
"python-mcp-tools/helper7.py": {
"captureGroups": 3,
"digest": "efdb60c4a9df5ddaff336e4c9899207cc9a2b9be231f596b9671e6001c8b6038"
},
"python-mcp-tools/helper8.py": {
"captureGroups": 3,
"digest": "894d62be48b6e8a67266cace71a247521a2e6d1a58ceaa138f9ffbaf829ec492"
},
"python-mcp-tools/helper9.py": {
"captureGroups": 3,
"digest": "25270485caa12492922ede0e4a2ed1e898f7d7862641e89cbc25ff0d31f77f18"
},
"python-mcp-tools/server.py": {
"captureGroups": 36,
"digest": "b65b1821625ca03577a06e5c90204a1513706f841386f8806310cd41eb67b9c1"
},
"python-member-access-for-loop/app.py": {
"captureGroups": 22,
"digest": "ec36378bb69d54cbe0d0a130e90f4122b7bc80191ec09e1af4437abca2d43ee6"
},
"python-member-access-for-loop/models/repo.py": {
"captureGroups": 6,
"digest": "54e050e7cecc288e367e97dc59280fd4febb614c7099894591e122901261a130"
},
"python-member-access-for-loop/models/user.py": {
"captureGroups": 6,
"digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2"
},
"python-member-calls/app.py": {
"captureGroups": 8,
"digest": "3d5282754e81dc800cf1bf11b414e7986b0133ae6545f242030002ececab4840"
},
"python-member-calls/user.py": {
"captureGroups": 9,
"digest": "485f993536f52252ad9b06971e99adb391fa95d005217c3563e41c48359fdf56"
},
"python-method-chain-binding/app.py": {
"captureGroups": 19,
"digest": "01fe4805f59723a5f163d26b7be3ed3e456eb3a093e3df3a8f034cecee22ebb9"
},
"python-method-chain-binding/models.py": {
"captureGroups": 34,
"digest": "062be597e84f6949477e56194ea5e875a6e1b8870e1af993d8c549d6f3de2d39"
},
"python-method-enrichment/app.py": {
"captureGroups": 13,
"digest": "88dfd417951b8083184f83da8c1e0c2b19700cfbf1f1ff203a904ebc00f50b30"
},
"python-method-enrichment/models.py": {
"captureGroups": 23,
"digest": "c177fc004563a84c1ccb5ed9cac366f184a7b5d0badcbfe7b3c204caf23bd533"
},
"python-module-export-vs-method-collision/app.py": {
"captureGroups": 14,
"digest": "d37707fc868b7b086fd2534356cef6f96d42f0ea4691847858d7a5ad03eabe34"
},
"python-module-export-vs-method-collision/mod.py": {
"captureGroups": 11,
"digest": "75158050a37ac0f4aa7427f87bfa1bb4d85933b92d023c9448d1fd21a787a3c3"
},
"python-module-import/app.py": {
"captureGroups": 15,
"digest": "1cda3d492b732bd620b2e158a6967b719433d52b045f9066305ec3ef99973b18"
},
"python-module-import/auth.py": {
"captureGroups": 11,
"digest": "0b99a5f6add0359f530c27c82f14ceb884849b24461870b8b517ab38a2714ae1"
},
"python-module-import/models.py": {
"captureGroups": 6,
"digest": "3e5160418c4e3b4c0a896bed89e731711aff3a66872d99bb5ebfcbd624d9a3d2"
},
"python-multi-level-mro/app.py": {
"captureGroups": 9,
"digest": "98bcec072e85a50303be141212b835322f5f9e53f7fe5d77b23d8ee524623e84"
},
"python-multi-level-mro/child.py": {
"captureGroups": 4,
"digest": "c85d867b0b206cd3e13e8dd94c83b49f13e0f2ddde7acd61785b9755f41d3d14"
},
"python-multi-level-mro/grandparent.py": {
"captureGroups": 7,
"digest": "f9f81d3a37c55b3e23bec3774405920afa29c5793c46860a98a06c5d0c7f0980"
},
"python-multi-level-mro/parent.py": {
"captureGroups": 4,
"digest": "05380e5d4d88a93546a3185c0f6598259cc5a1c271ec08a130cf72cf9107023d"
},
"python-multi-segment-ancestor-import/backend/auth_utils.py": {
"captureGroups": 6,
"digest": "45f881e9deb28e29d912924a6d108378cc7f5046ed84b0302d9e4b36c5085f8c"
},
"python-multi-segment-ancestor-import/backend/routers/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-multi-segment-ancestor-import/backend/routers/alerts.py": {
"captureGroups": 3,
"digest": "be1e4339d6f7d31c86c60fa950ca455a38b7ec18a223f5b2fef4dadbae0d3328"
},
"python-multi-segment-ancestor-import/backend/routers/cron.py": {
"captureGroups": 25,
"digest": "1d7405bb1dc911b0cbd03bde1c86a49bc54079a4de9f74c00196e43795ec4bf0"
},
"python-multi-segment-ancestor-import/backend/services/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-multi-segment-ancestor-import/backend/services/alerts.py": {
"captureGroups": 3,
"digest": "bcbe9f5923dda8b54c1cc1a07c38439ce93d5ce13101775c20ff6e4de9afb404"
},
"python-multi-segment-ancestor-import/backend/services/sync.py": {
"captureGroups": 5,
"digest": "3c8bb78fafe4c0797d9d1ca162911d96f7b6fd3a5a037c26952b7f0287610150"
},
"python-named-imports/app.py": {
"captureGroups": 5,
"digest": "179248a29a48abba318bece3fe6eeca436db9544126684e203ad27983dc41695"
},
"python-named-imports/format_prefix.py": {
"captureGroups": 3,
"digest": "199776174b6aa80ea46969db4b427dbd4d6be0d4d6a96d62f05d984556a2b09f"
},
"python-named-imports/format_upper.py": {
"captureGroups": 4,
"digest": "3f0a4801427bc126dff1d700a91d9133f3e59d3db4db26eccfb3276258cb4fea"
},
"python-nullable-chain/app.py": {
"captureGroups": 31,
"digest": "d5a8af8929a21808c3570cdc175b8412a5925b41cdace728ffc0179cdb2bc484"
},
"python-nullable-chain/repo.py": {
"captureGroups": 7,
"digest": "8c79e31ea4557abf29e4e1ec087db258e48afa3f9cc1e637291d0d5d49914980"
},
"python-nullable-chain/user.py": {
"captureGroups": 7,
"digest": "967ff5732d066c516ad0bfddd87696598ab9c2c4b55c4463641db14af4cdfb78"
},
"python-nullable-receiver/app.py": {
"captureGroups": 23,
"digest": "7f22953242a4402b31ebcd6219d4976fd706c2775ef60d85f0911900e08cd647"
},
"python-nullable-receiver/repo.py": {
"captureGroups": 6,
"digest": "0d2704cfa14e431395973cc82b582f9fae43a8ce67d5548ab5df62b3fa8d1b7e"
},
"python-nullable-receiver/user.py": {
"captureGroups": 6,
"digest": "4dd0a9d0f5797bae1c281a07145ddcc4162eae1ea16208e85edb74cd98e085f9"
},
"python-overload-dispatch/app.py": {
"captureGroups": 21,
"digest": "37fa5251fc1914d95fab2c6aae6d97b460a47f46c5b74415a86c955f4ed78626"
},
"python-overload-dispatch/service.py": {
"captureGroups": 28,
"digest": "a873c2cebeb1092fe21b75b2109052fbb66335a378dd5468b32a57a4644ad14b"
},
"python-parent-resolution/models/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-parent-resolution/models/base.py": {
"captureGroups": 7,
"digest": "f0384bd6ecb7d1a9ad2306358917b7295c71ea8f1bcea818fd39c56d2c28c7e9"
},
"python-parent-resolution/models/user.py": {
"captureGroups": 8,
"digest": "0dbcf175be44961f7a7c02e21167f10063772297633173236ce49f970bb395a4"
},
"python-pkg/models/base.py": {
"captureGroups": 9,
"digest": "4984ee01b7a9fefe622195f0e4925823c0e62a1714ca2dda5cd8250e5e45fa7c"
},
"python-pkg/models/user.py": {
"captureGroups": 7,
"digest": "8c340fe8e46b5adf03c82e736822c501983c087b85dc72bdf6d12b75faa843ab"
},
"python-pkg/services/auth.py": {
"captureGroups": 9,
"digest": "3c9e44e7870643e475a67f562c15a42d58ade2b47a6c43ad665ef73e53e765ff"
},
"python-pkg/utils/helpers.py": {
"captureGroups": 7,
"digest": "9a12c0b8c2523d4dfd5919e04cfe2bf4e9f6b84fe1a83a146f58996b7d453bb5"
},
"python-plain-import-alias/app.py": {
"captureGroups": 17,
"digest": "a419c716b0d13787ce1f3cc2adb5ab2ab77604cb8348a9b27aa56443984ab0b2"
},
"python-plain-import-alias/auth.py": {
"captureGroups": 6,
"digest": "51a9f92e1dae910b08b86f506c174ad2f60dfc1d2d5f66d22838978d7d424b48"
},
"python-plain-import-alias/models.py": {
"captureGroups": 11,
"digest": "abf5fcdf7cc473efa5a149319b0fc3d14b3ae91a2e9a14d621243c5ed7fafafa"
},
"python-qualified-constructor/main.py": {
"captureGroups": 9,
"digest": "9e4d51c06d75721e3c07898124ef85139d52d9f20d6df6bae557aff98a3ed207"
},
"python-qualified-constructor/models.py": {
"captureGroups": 13,
"digest": "94e738bd516d6e1fa1ca130fc231157071c04aa3b2ad74db6dabbacd7b7842a7"
},
"python-receiver-resolution/app.py": {
"captureGroups": 15,
"digest": "806934204693a06760f3c74e8c68145eda5cf19d7903130646e152ea14fc7a98"
},
"python-receiver-resolution/repo.py": {
"captureGroups": 6,
"digest": "0d2704cfa14e431395973cc82b582f9fae43a8ce67d5548ab5df62b3fa8d1b7e"
},
"python-receiver-resolution/user.py": {
"captureGroups": 6,
"digest": "4dd0a9d0f5797bae1c281a07145ddcc4162eae1ea16208e85edb74cd98e085f9"
},
"python-reexport-chain/app.py": {
"captureGroups": 13,
"digest": "a30e5ce2de19dcb4d099c592e96d2cc86ffb859d8bd55e0eddd13aeeeac67ed8"
},
"python-reexport-chain/models/__init__.py": {
"captureGroups": 3,
"digest": "ee0d8506fbaaf9cbf3a2f78bd34b7e688eb01aed590408cc2d6c59a4ee3bb4d8"
},
"python-reexport-chain/models/base.py": {
"captureGroups": 13,
"digest": "4ea6fa3504e37231c8d395259009df5d4adda31e3ea0bfc28cd3c3eabdd68280"
},
"python-return-type-inference/app.py": {
"captureGroups": 8,
"digest": "741f690b6330491303b9b58cb31027a33600973265b59428facfefbabf0cf7e1"
},
"python-return-type-inference/models.py": {
"captureGroups": 12,
"digest": "a7f11709f76dcc9311554b4fe1dd4e95f5d6434e6232627b4c370f21e194bd69"
},
"python-return-type-inference/service.py": {
"captureGroups": 7,
"digest": "81f8d5783bfc95a9f83f02abd15f7bbc0869aaca964c62b40653cc05ab73ebcb"
},
"python-same-file-method-collision/app.py": {
"captureGroups": 17,
"digest": "cd1e6bd1cea7de20c9317fa650bb0b3492df81d736d24142a0a3f082834c507a"
},
"python-same-file-method-collision/models.py": {
"captureGroups": 21,
"digest": "3c5ef9ae13442f9c77a61c4fc6b76ebc580cdd5857810cc484f6cb768fa9d727"
},
"python-same-name-collision/metrics.py": {
"captureGroups": 7,
"digest": "57482d976bc3d4f232064b55f809507a9e00ec4f75ee519691034227c666312e"
},
"python-same-name-collision/router.py": {
"captureGroups": 10,
"digest": "e9248113f2dbae22dff443551daa131136c69f00ea0fb912a6419fcb1c1e7833"
},
"python-self-this-resolution/models/repo.py": {
"captureGroups": 7,
"digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215"
},
"python-self-this-resolution/models/user.py": {
"captureGroups": 12,
"digest": "b23ad4122f7fdb5b3f9b047582229a9ea001a29452035974112d4f425ef86cf4"
},
"python-static-class-methods/app.py": {
"captureGroups": 14,
"digest": "b397708c05d101d0d3978313f65184c9f181a179b803d242b1e585ac2605fe4a"
},
"python-static-class-methods/service.py": {
"captureGroups": 27,
"digest": "9539dd5884b6b5ef3ad14b9d06837a495b4d3908462ec37464524c259a2cbd75"
},
"python-super-resolution/models/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-super-resolution/models/base.py": {
"captureGroups": 7,
"digest": "f0384bd6ecb7d1a9ad2306358917b7295c71ea8f1bcea818fd39c56d2c28c7e9"
},
"python-super-resolution/models/repo.py": {
"captureGroups": 7,
"digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215"
},
"python-super-resolution/models/user.py": {
"captureGroups": 10,
"digest": "2ab193907b46f0fd20f18e0b3b019986ae34f6252bff38145ebb40922bf7eb00"
},
"python-variadic-resolution/app.py": {
"captureGroups": 5,
"digest": "62b16647599de4db600ece073131025de23bf692d1585d8b7f1bfc3fae2f3dcb"
},
"python-variadic-resolution/logger.py": {
"captureGroups": 5,
"digest": "5995f1a01f8354c3d854d4f4dc51fb4a2919bdcb22e90eb0a64431eb03e5804c"
},
"python-walrus-chain/app.py": {
"captureGroups": 33,
"digest": "07590672df6cd4d1069ec346033fa29c116d42c841ec68da8d22afd44effb938"
},
"python-walrus-chain/repo.py": {
"captureGroups": 7,
"digest": "8c79e31ea4557abf29e4e1ec087db258e48afa3f9cc1e637291d0d5d49914980"
},
"python-walrus-chain/user.py": {
"captureGroups": 7,
"digest": "967ff5732d066c516ad0bfddd87696598ab9c2c4b55c4463641db14af4cdfb78"
},
"python-walrus-operator/main.py": {
"captureGroups": 7,
"digest": "4df7ea089c43552ca4ea5a51f8e985d11d351b86949efb2f7ebefdf6a9ffd689"
},
"python-walrus-operator/models.py": {
"captureGroups": 16,
"digest": "ccd7c8c00bc2fe3bd2bad667e2448591c3508e9eb91a0005a9148a2d3734ac98"
},
"python-write-access/models.py": {
"captureGroups": 11,
"digest": "d1b714cb6654f83df3bb40830ced44f2950a122270c9e379f732943d57688151"
},
"python-write-access/service.py": {
"captureGroups": 9,
"digest": "0aa940e3428c35b1324544e9bcc75c166732b63b3e65e6faf460bc3d3a9571ec"
},
"synthetic:dao-20": {
"captureGroups": 473,
"digest": "27a7e0ea629f7ed002f1bd254126c819125900b27e0818d879e8fdaa8690aab9"
}
}

View file

@ -0,0 +1,314 @@
{
"ruby-ambiguous/lib/user_handler.rb": {
"captureGroups": 9,
"digest": "282d98829c29708081e96207dc0a6d82142400444a1345e162287e6b2c23d019"
},
"ruby-ambiguous/models/handler.rb": {
"captureGroups": 6,
"digest": "4609ec03a86a4f4f2a93af9dd11437363daf1b3139ebdce366a8f49d47ea71b1"
},
"ruby-ambiguous/other/handler.rb": {
"captureGroups": 6,
"digest": "d13c8ad253a9310d0bed45730135a947e67dec788686bd08710902eafc307237"
},
"ruby-app/lib/base_model.rb": {
"captureGroups": 20,
"digest": "999f23c116bf4c0a25834e3fff1b12d5ec1bc5616a29e75716ba753dc253a62c"
},
"ruby-app/lib/concerns/cacheable.rb": {
"captureGroups": 6,
"digest": "cc879f2fa31b1d50b7ba33f52c6dd68ba0ff070ffbc3c2d0293510f88643b373"
},
"ruby-app/lib/concerns/loggable.rb": {
"captureGroups": 6,
"digest": "049ea9beaf9c55b55bd836d31af4c8806ce24bb805261ae4d62575b4a19118bc"
},
"ruby-app/lib/concerns/serializable.rb": {
"captureGroups": 6,
"digest": "5a5a6e8cb7bcd32d828763152b8107e5ee824a5e6244ee55f85925f23e520b02"
},
"ruby-app/lib/service.rb": {
"captureGroups": 13,
"digest": "49bd37b2138dc17820351c126a7a75622cb564e8dd15a9b5f79fcc2d0844474d"
},
"ruby-app/lib/user.rb": {
"captureGroups": 23,
"digest": "38b79d3ef8e804c56e3819804129a6fc9c6bbb8d93d2a96413909f4884e99d94"
},
"ruby-call-result-binding/app.rb": {
"captureGroups": 17,
"digest": "42a4ffb39fa090608cb0f60e8c31d2c51c7515a4ee6ae038e9accbac0d2e5488"
},
"ruby-calls/lib/one_arg.rb": {
"captureGroups": 7,
"digest": "a04d2c55b5738a824bfa4139925cade1d437bf0a1a91ebf044c5098e2e5f9b58"
},
"ruby-calls/lib/service.rb": {
"captureGroups": 11,
"digest": "7a4d964547d3ac645274c65fcbceba3f6852286537af2452c87d75bbde3495ab"
},
"ruby-calls/lib/two_args.rb": {
"captureGroups": 7,
"digest": "533578cb1111c137d4cc660d1667ba43f649799a9fbd6139a20ac389d16477bf"
},
"ruby-chain-call/lib/app.rb": {
"captureGroups": 13,
"digest": "ea7f8a06ec8cb89b47a3c5ebfce79211a51abf643fe9a48e480729b8323491c4"
},
"ruby-chain-call/lib/repo.rb": {
"captureGroups": 6,
"digest": "9effd68932555f44968ec89ea1fb0bff4719185c777e856ae167a2289fa72239"
},
"ruby-chain-call/lib/user.rb": {
"captureGroups": 6,
"digest": "5efcef8e4c0dc82ddaa88cf6ed51c8a79e86e34e06e14c4363373444da5d5292"
},
"ruby-chain-call/lib/user_service.rb": {
"captureGroups": 11,
"digest": "bfd4e260c15d1e5da32af458a4304ffa21e4e6e22133fa8dd34ad14e50beb0f6"
},
"ruby-child-extends-parent/lib/app.rb": {
"captureGroups": 12,
"digest": "8a75c4468b2b1f033620e7d233071340c129a06db4b8ca85a54293efdc5b989f"
},
"ruby-child-extends-parent/lib/child.rb": {
"captureGroups": 5,
"digest": "6b0b88e4101367fb23107124b5ccea5c54632cc39ec272c5816fc6119b680910"
},
"ruby-child-extends-parent/lib/parent.rb": {
"captureGroups": 6,
"digest": "e0b6dd3034cc87ed2ae1e46da9681e58074f3b2d2778e6584869bf49a3dd6b7e"
},
"ruby-constant-constructor/app.rb": {
"captureGroups": 7,
"digest": "cb37edd7c73b7759607850b774627ff2ad223458c611171d666cee83e4de99cd"
},
"ruby-constant-constructor/models.rb": {
"captureGroups": 9,
"digest": "580c49087a657582485e87d528777d2b60e6e29d3d5e652e777c23ba35186f7b"
},
"ruby-constant-factory-call/admin_service.rb": {
"captureGroups": 9,
"digest": "a62fbc9faded8b59e1bc529d72e6132187a493475d99ff6163cc71cd1de773f4"
},
"ruby-constant-factory-call/app.rb": {
"captureGroups": 14,
"digest": "f3b9bc3394854593513e17ef3c0e37375aab959d666b4aed4df443377608eb64"
},
"ruby-constant-factory-call/user_service.rb": {
"captureGroups": 9,
"digest": "d9d74ff4dae50c699e4f4f874c4aa75fa5c27e57ffc4fb170aafca4d333bb2a7"
},
"ruby-constructor-type-inference/models/repo.rb": {
"captureGroups": 9,
"digest": "b0f3b5728c603a2056613091ac75d5014d99c4cdb2d192285c2707117bb46d0c"
},
"ruby-constructor-type-inference/models/user.rb": {
"captureGroups": 9,
"digest": "dc7cc04f621b487fd24f417281a6a77b6b5dee234872b98c7d52ea719a991205"
},
"ruby-constructor-type-inference/services/app.rb": {
"captureGroups": 26,
"digest": "afa655d9a3c7ebea02b529d33904575692a53531a619c3fd34634f0bb43855e4"
},
"ruby-default-params/app.rb": {
"captureGroups": 7,
"digest": "84779fa164cbc83863f6ae1552ca8ed19f21a18cb410b26ea581132c21d88a6a"
},
"ruby-field-type-disambig/address.rb": {
"captureGroups": 8,
"digest": "cfb08d28049718393f211454b0c0148aaa82d7093915f0d8650a224d8ff687e3"
},
"ruby-field-type-disambig/service.rb": {
"captureGroups": 8,
"digest": "bb2449fe1f87d23fca7db7507987d32daae36cbee8166fe9990006934b9d05a5"
},
"ruby-field-type-disambig/user.rb": {
"captureGroups": 13,
"digest": "7f4ed543946d9e243aa83e122303558aa336cff91e5323db09502999f64298d3"
},
"ruby-field-types/models.rb": {
"captureGroups": 19,
"digest": "6673d1c2b299edee80c5d7ff0dddb8741e6b03070fbc119a5c1f14598ed8b005"
},
"ruby-field-types/service.rb": {
"captureGroups": 8,
"digest": "9dcde083c602b85bf41561623dc7e6af16718cd1c8bacf00ac4b8d917a1557cd"
},
"ruby-for-in-loop/app.rb": {
"captureGroups": 8,
"digest": "b584394fac8f939f3d4d1170c373da4332bfeffa641e64e997d108e85a4c5ec3"
},
"ruby-for-in-loop/repo.rb": {
"captureGroups": 9,
"digest": "fb4c12f847eca414066c5219a68c2dd48167a174d28788da118814ee8931ccfd"
},
"ruby-for-in-loop/user.rb": {
"captureGroups": 9,
"digest": "b6f4c241293057f3d4505cfec9d63376d1f598599dc6bf0a38ec0bef242dda5a"
},
"ruby-grandparent-resolution/lib/app.rb": {
"captureGroups": 10,
"digest": "118450b872d82f6762e0081b85afaff8c28f683395ecd3c1eec7efdf8dd420a6"
},
"ruby-grandparent-resolution/lib/models/a.rb": {
"captureGroups": 10,
"digest": "83bfdb09dd8a1a0bda06c84092041e257502ca37ae393c57ec2ff43b67d699ae"
},
"ruby-grandparent-resolution/lib/models/b.rb": {
"captureGroups": 5,
"digest": "fc9e9541d4c3e85be94cc8be6559d6ee25b9d7178aafa7b50848ab00409446dc"
},
"ruby-grandparent-resolution/lib/models/c.rb": {
"captureGroups": 5,
"digest": "d507a997c8d022705c15d55c3bbc6a6134353c1114a2de4629f4b4b4f9080ca0"
},
"ruby-grandparent-resolution/lib/models/greeting.rb": {
"captureGroups": 6,
"digest": "a5f34e078d09145f2c2921610af56a1dd8273705d31446002d6a28aff1d32abc"
},
"ruby-local-shadow/lib/app.rb": {
"captureGroups": 8,
"digest": "6413cadbe85841eb6f06f42e8a7dd3902cc0d2908971b61c1a1a5e0e221afec9"
},
"ruby-local-shadow/lib/utils.rb": {
"captureGroups": 6,
"digest": "b2c5cce7a167d427de4fea1b955b9ec6401ab70f0fbd61a44fb2051332beb82d"
},
"ruby-member-calls/lib/app.rb": {
"captureGroups": 12,
"digest": "78f4ec67d744302bf2827c0b61de167285fdb95794a34738898a8cb92d2529a5"
},
"ruby-member-calls/lib/user.rb": {
"captureGroups": 6,
"digest": "c621dd1859fe820beedf3fb98f4caa483ca262ea6936c8d6253bfabd56b5d557"
},
"ruby-method-chain-binding/app.rb": {
"captureGroups": 39,
"digest": "54fc82a9a0a67ccff1d5ca3055c93044e79e6cd2972d943f9c2e90f1f2198716"
},
"ruby-method-enrichment/lib/animal.rb": {
"captureGroups": 27,
"digest": "5feb199009fcc4923665fcd4a8f3173388be58ca6ae53f19de6795cd2521d397"
},
"ruby-method-enrichment/lib/app.rb": {
"captureGroups": 14,
"digest": "941438b3abc3d34ad79e62eb3bf2eee785ac01fd5034f6f40a6911895d79c9ac"
},
"ruby-namespaced-constructor/app.rb": {
"captureGroups": 8,
"digest": "8f6555980417465c870a16742eb2d49fcb8a31e26777c11b926180674aa93e97"
},
"ruby-namespaced-constructor/models/user_service.rb": {
"captureGroups": 12,
"digest": "c10f36dbbbe2be16fc3fccbebb7ee79668ec7a77b75adbd5281ded31893d49de"
},
"ruby-overload-dispatch/lib/app.rb": {
"captureGroups": 10,
"digest": "288d5386cf37fb76b52a94bc7da6bf8e7843830ebbb01fcd8100d1590c0e3f72"
},
"ruby-overload-dispatch/lib/formatter.rb": {
"captureGroups": 11,
"digest": "3e210571081b6977ea7d8b23f1678b57bedb2414cf5f6730466bf7b56829857c"
},
"ruby-parent-resolution/lib/models/base_model.rb": {
"captureGroups": 6,
"digest": "e108bf68441486968cff17aabfc3489fd295f15d7ad32cac8dad9c1b56e6029b"
},
"ruby-parent-resolution/lib/models/serializable.rb": {
"captureGroups": 6,
"digest": "4b415b1bcb31b0290a01279f86305459f53e43efb388e96d6e7b4bf804bd12d8"
},
"ruby-parent-resolution/lib/models/user.rb": {
"captureGroups": 8,
"digest": "6bb1c81445d4a3f41b6a23e6a20ee4602a261ee5405c3f463dd81c6741bc8e00"
},
"ruby-qualified-types/lib/admin/user.rb": {
"captureGroups": 8,
"digest": "1bceb829c2429e1415c296ea97a2475203c3cbf69c07123186f4c277a44b2f9f"
},
"ruby-qualified-types/lib/services/auth/user.rb": {
"captureGroups": 10,
"digest": "21fbc89550147f66806eb9e506870fcf5f4bec23876bdce069897d905e0ae307"
},
"ruby-return-type/app.rb": {
"captureGroups": 17,
"digest": "0db7356580a8ac5addec69a3259e5a78028e01b44f5e36316a6f76172929f829"
},
"ruby-return-type/models.rb": {
"captureGroups": 14,
"digest": "b731080e81701e48bbe0eff87dcb0286d266626037dc1e422cc268ef1a644722"
},
"ruby-return-type/repo.rb": {
"captureGroups": 14,
"digest": "70875215143713279e9595e89b8c5d7d2c642d0660365772b64daf5b8b487b4f"
},
"ruby-self-this-resolution/lib/models/repo.rb": {
"captureGroups": 6,
"digest": "9effd68932555f44968ec89ea1fb0bff4719185c777e856ae167a2289fa72239"
},
"ruby-self-this-resolution/lib/models/user.rb": {
"captureGroups": 10,
"digest": "17c3c937d7d40cffec95393e6b7f2886b44a9117f048fb72eafa88140320c45e"
},
"ruby-sequential-mixin/lib/account.rb": {
"captureGroups": 27,
"digest": "e426aff3f6a6b4e403a6bf64aa4c15ddea6afe43b18754bfb71103653b1df8b3"
},
"ruby-sequential-mixin/lib/greetable.rb": {
"captureGroups": 6,
"digest": "f56ff0b87abb7463dcf5fdbe18c1cf74c11595853ff04ff8d929cc8d5cb71ca5"
},
"ruby-sequential-mixin/lib/logger_mixin.rb": {
"captureGroups": 7,
"digest": "c6edbab52ae7a58d89bd11da498d99a7cf0eaaeab531c56774b1a3fee1c39d1e"
},
"ruby-sequential-mixin/lib/prepended_override.rb": {
"captureGroups": 9,
"digest": "90bdfeb850c14a1f85ac69b194c4a37db196d4709b503e00a31968c550a66746"
},
"ruby-sequential-mixin/lib/usage.rb": {
"captureGroups": 14,
"digest": "c8e1cd74bf9fd8db677fc8e2adbcc8b7eb303884d5339aa964350f81167935bc"
},
"ruby-super-resolution/lib/models/base_model.rb": {
"captureGroups": 6,
"digest": "e108bf68441486968cff17aabfc3489fd295f15d7ad32cac8dad9c1b56e6029b"
},
"ruby-super-resolution/lib/models/repo.rb": {
"captureGroups": 6,
"digest": "9effd68932555f44968ec89ea1fb0bff4719185c777e856ae167a2289fa72239"
},
"ruby-super-resolution/lib/models/user.rb": {
"captureGroups": 8,
"digest": "5122d102ff7e2e0b9f1396ee5b6ddab33a866aa3c29af10301dbf673ffaa19af"
},
"ruby-write-access/models.rb": {
"captureGroups": 13,
"digest": "106fe23a380801055a2ef642f0b92d4552ce074321ebe831601f2cb89a8d8529"
},
"ruby-write-access/service.rb": {
"captureGroups": 18,
"digest": "b39aef2dd2721626e0c1718ea150455bc54522022ba45ce0d235f2e33e861be9"
},
"ruby-yard-annotations/models.rb": {
"captureGroups": 14,
"digest": "1906ddedab8b6f352468a13cefd3d018889492b8dc983f854ccbe7a2b84a938f"
},
"ruby-yard-annotations/service.rb": {
"captureGroups": 13,
"digest": "f0864dc29de46bc9ae3c15cae5714490aac6536d8da0704a353f2300df75bb63"
},
"ruby-yard-generics/models.rb": {
"captureGroups": 17,
"digest": "3a0722a48ac72ad10a492d10c4728a805990d07789e5b20a239a6c95a914d23f"
},
"ruby-yard-generics/service.rb": {
"captureGroups": 18,
"digest": "040e4c3a4091238c8b59089f4b4e92d59879dbe5053131395a5e1ac6ad5f1d09"
},
"synthetic:dao-20": {
"captureGroups": 161,
"digest": "99da729084d5a08425f04078fb633b35b7256e861b67bce095af59cb42e151fe"
}
}

View file

@ -0,0 +1,454 @@
{
"rust-abstract-dispatch/src/lib.rs": {
"captureGroups": 26,
"digest": "8723f4d9b1c4824be8ec22c28b203113b781fc05cffcd80a6b15efe60a0f927d"
},
"rust-abstract-dispatch/src/main.rs": {
"captureGroups": 19,
"digest": "f0483e7bed3772013ed48efea6071e4959603e5ceb9b3786e4d334c7a1ccf76a"
},
"rust-alias-imports/src/main.rs": {
"captureGroups": 19,
"digest": "ffa9d81433d8d4931f07ba0151b2349f0bbf876e7b327a5ffeeaf3ed18a43abf"
},
"rust-alias-imports/src/models.rs": {
"captureGroups": 21,
"digest": "fa89b58c454471da61f7e1585854f42e2f331517094420cf7f76fdf5b898795e"
},
"rust-ambiguous/src/main.rs": {
"captureGroups": 12,
"digest": "7d29d164d6f64e2bcac4bba8d4b86d725f696a836341b5265a5dce9543b1691e"
},
"rust-ambiguous/src/models/handler.rs": {
"captureGroups": 9,
"digest": "5a1729f00b573f6abb214866ae618cd32fe4c166591327f4de782e262af39266"
},
"rust-ambiguous/src/models/mod.rs": {
"captureGroups": 3,
"digest": "f83985431e11f24a46d28f6c5256dd2e7b6d17a1079dd51bf8c4fe4f9087be11"
},
"rust-ambiguous/src/other/handler.rs": {
"captureGroups": 9,
"digest": "cbcf55909786187d156c6ce9e94e37602897a3b1515aa80bb15110014f31d048"
},
"rust-ambiguous/src/other/mod.rs": {
"captureGroups": 3,
"digest": "f83985431e11f24a46d28f6c5256dd2e7b6d17a1079dd51bf8c4fe4f9087be11"
},
"rust-ambiguous/src/services/mod.rs": {
"captureGroups": 8,
"digest": "225b1a2de0f98873a99c669275ecce6a7f6f0d04f2b50f6a15f7546c06f2e295"
},
"rust-assignment-chain/src/main.rs": {
"captureGroups": 35,
"digest": "ba2d21b5126b59c0cfe9d99427ee6726eab0e3f2d19022e4b946bddf11730b42"
},
"rust-assignment-chain/src/repo.rs": {
"captureGroups": 10,
"digest": "24dcd450677a5885db0d93973e431e47b41e6866ad3fc7cb686f7a021000c17e"
},
"rust-assignment-chain/src/user.rs": {
"captureGroups": 10,
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
},
"rust-async-binding/src/main.rs": {
"captureGroups": 33,
"digest": "4c8c0c21c2dffe6efaf91d26e92c1bafee95f940909fa4d5a73ad0c07b5aee2f"
},
"rust-async-binding/src/repo.rs": {
"captureGroups": 11,
"digest": "43911a94194a6771769f9bad2a81108fa84bb842aaeaf7c1e17f22beb49b9c6e"
},
"rust-async-binding/src/user.rs": {
"captureGroups": 11,
"digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9"
},
"rust-call-result-binding/src/main.rs": {
"captureGroups": 11,
"digest": "db75d1b522e5d3574037b692e51423f5f71b6bba011aa120e33d0c6fbc6c1033"
},
"rust-call-result-binding/src/models.rs": {
"captureGroups": 19,
"digest": "550f189e1374dc52599482fb63c8363a7839b4a7514aa6e911c354b3a92926a9"
},
"rust-calls/src/main.rs": {
"captureGroups": 9,
"digest": "2ec004ce1d77e3aee80ae1332bc0ab64fb366deffbac84f84ff1861286edc4e5"
},
"rust-calls/src/onearg/mod.rs": {
"captureGroups": 6,
"digest": "5a2b4645b0dcebfb9eada1a5b168b99830316859854d1f9c55654a3ab0211f0c"
},
"rust-calls/src/zeroarg/mod.rs": {
"captureGroups": 5,
"digest": "aae68fe5023fa93180b05ce8034654bfa1d2d95bc7eea67625e2e3a3cff6ebee"
},
"rust-chain-call/src/main.rs": {
"captureGroups": 23,
"digest": "c99d5d1f0aaf873eec47268f852fc265e4f1e2d2be08b76be653e49f43ffc5df"
},
"rust-chain-call/src/models/mod.rs": {
"captureGroups": 3,
"digest": "c96e9c3df9991cd8e40612b0f1a5fc83a42013cf0870458209fe80c41dd5abd1"
},
"rust-chain-call/src/models/repo.rs": {
"captureGroups": 11,
"digest": "43911a94194a6771769f9bad2a81108fa84bb842aaeaf7c1e17f22beb49b9c6e"
},
"rust-chain-call/src/models/user.rs": {
"captureGroups": 11,
"digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9"
},
"rust-child-extends-parent/src/child.rs": {
"captureGroups": 12,
"digest": "b357cb50ed1d09f5d218261ed640f73c393f05be36c959e001987d8423ab4194"
},
"rust-child-extends-parent/src/main.rs": {
"captureGroups": 17,
"digest": "21698d0650c872cb7a2f0849aef47d266ffb6a851d01286174d76f74a25acd1e"
},
"rust-child-extends-parent/src/parent.rs": {
"captureGroups": 7,
"digest": "89bbfe22788bf85e8fda4ad0d1c8dc14f4ec983624e59b58a68852e093290b25"
},
"rust-constructor-type-inference/src/main.rs": {
"captureGroups": 21,
"digest": "5cb66a46d7a52800325aab132297e002ccbda7da322ee84d9b95bd7367842c7c"
},
"rust-constructor-type-inference/src/repo.rs": {
"captureGroups": 15,
"digest": "5c0858427be24f2cc6ae346b9dc8294d85a530bcf2f529829d757b69f768dc65"
},
"rust-constructor-type-inference/src/user.rs": {
"captureGroups": 15,
"digest": "043cd8b9341ab299750f8d07f1d1ca34b714c4ecf69acba80ec827e93e3852c0"
},
"rust-deep-field-chain/models.rs": {
"captureGroups": 24,
"digest": "fe28a5861492fc4b20d911e44abc246ec33e4f3305fe4bd7e95c952972f17564"
},
"rust-deep-field-chain/service.rs": {
"captureGroups": 15,
"digest": "143fcbb0bda81b945ecb1b3a692fb35ccc1e7b2ee1eea64f20ec548b432a53fb"
},
"rust-default-constructor/src/main.rs": {
"captureGroups": 34,
"digest": "6516c6b21d2cca74b18ee443ca0049228b4832efc551ca23e5cf3098c95d34f4"
},
"rust-default-constructor/src/repo.rs": {
"captureGroups": 21,
"digest": "968aaf37e492e999c3699fb6eb8f3ee3d021bc112fa205198469e48fd65aeef8"
},
"rust-default-constructor/src/user.rs": {
"captureGroups": 21,
"digest": "fe375f3a12ea8c05743816c7d218d50fc1898968603a00906194ef1833e247cd"
},
"rust-err-unwrap/src/error.rs": {
"captureGroups": 9,
"digest": "798c8e01c6e54792ba69e845248efc8abf0cba38fa3d16fb8e0d1f6dd2ad2b7e"
},
"rust-err-unwrap/src/main.rs": {
"captureGroups": 24,
"digest": "e4c452ce79e6e09f24d77acb9537f38b46afb4c59661192d0a4bc496a2b753bb"
},
"rust-err-unwrap/src/repo.rs": {
"captureGroups": 9,
"digest": "09f0d2905c2fc05043fa767b277a23b787aad4b9c0fe22bc5569ca9660f7f18a"
},
"rust-err-unwrap/src/user.rs": {
"captureGroups": 9,
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
},
"rust-field-types/models.rs": {
"captureGroups": 21,
"digest": "f6c64476fd1b30699d0c35cf7b64cd4210b3ae2158f29ad0faae614b1b218c83"
},
"rust-field-types/service.rs": {
"captureGroups": 10,
"digest": "03f3b9175fc383450ab97e7e82a3e0feff4ffebdd78f4804717c8f64045c080c"
},
"rust-for-call-expr/src/main.rs": {
"captureGroups": 24,
"digest": "292eba3d86490a2ee600ebe4d9e19434204b43706af459c25ae82c2b646da5f9"
},
"rust-for-call-expr/src/repo.rs": {
"captureGroups": 13,
"digest": "2b7e531ed136a976228b8073066f1fd9ba30e9409c10d5708c544a617ad2f31c"
},
"rust-for-call-expr/src/user.rs": {
"captureGroups": 13,
"digest": "9e5ee9a073c988caace8946b7008e56e7a1cfa852972a28387f8bf0c52b73555"
},
"rust-for-loop/src/main.rs": {
"captureGroups": 24,
"digest": "9622a9b47737283123a93511231529524ec82a54c5a97c17dc81dcc6e8f4912c"
},
"rust-for-loop/src/repo.rs": {
"captureGroups": 9,
"digest": "09f0d2905c2fc05043fa767b277a23b787aad4b9c0fe22bc5569ca9660f7f18a"
},
"rust-for-loop/src/user.rs": {
"captureGroups": 9,
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
},
"rust-grouped-imports/src/helpers/mod.rs": {
"captureGroups": 13,
"digest": "04ce0efd7458e7ff624e5611d34bbb0c01e77259108e0477895bba61e9568249"
},
"rust-grouped-imports/src/main.rs": {
"captureGroups": 13,
"digest": "17fff90e2627d094f24ffb7cd06b258d3f69b026b52f892c66132d061d031b1e"
},
"rust-if-let-unwrap/models/mod.rs": {
"captureGroups": 1,
"digest": "6397af74a6370d7ed3225ca11da5069e16f7620639acd18ad0e86109304cefbb"
},
"rust-if-let-unwrap/models/repo.rs": {
"captureGroups": 1,
"digest": "afefc4e62e0fa4b3becf9efc796e2afdc1f5b8b3f1566f2a7fa7b0aecf3d6477"
},
"rust-if-let-unwrap/models/user.rs": {
"captureGroups": 1,
"digest": "abf2026b72dacf36eca93895d2601c900424f30ff7f59f904f0dcf1fa377829c"
},
"rust-if-let-unwrap/src/main.rs": {
"captureGroups": 15,
"digest": "3a45c99bbfc3ba2117d333127b3031697623bb044a1f04ce5381ff1366fe7468"
},
"rust-if-let-unwrap/src/repo.rs": {
"captureGroups": 9,
"digest": "09f0d2905c2fc05043fa767b277a23b787aad4b9c0fe22bc5569ca9660f7f18a"
},
"rust-if-let-unwrap/src/user.rs": {
"captureGroups": 9,
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
},
"rust-if-let/main.rs": {
"captureGroups": 32,
"digest": "dd9d7112782afac2c55cc9808ea3893551efba8ff4af0c83a55610d5533d24b7"
},
"rust-if-let/models.rs": {
"captureGroups": 20,
"digest": "d8c1eb57431b915dd5c9055d8451054a454e340c4842628e38e4d46f69471abd"
},
"rust-iter-for-loop/src/main.rs": {
"captureGroups": 28,
"digest": "7443b6c9231e54de5afd5f6fb52fd0a9709299a650615119a5be68de43287a88"
},
"rust-iter-for-loop/src/repo.rs": {
"captureGroups": 9,
"digest": "09f0d2905c2fc05043fa767b277a23b787aad4b9c0fe22bc5569ca9660f7f18a"
},
"rust-iter-for-loop/src/user.rs": {
"captureGroups": 9,
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
},
"rust-local-shadow/src/main.rs": {
"captureGroups": 15,
"digest": "a9903b31883988b89bf634ea2bda7de522f256d06b0d80a65050a3fb4fae4a80"
},
"rust-local-shadow/src/utils.rs": {
"captureGroups": 5,
"digest": "8007a597a21f60c078ceaaa96c660d157d4f1c27401345145be0b5b8b1fb4c0d"
},
"rust-match-unwrap/src/main.rs": {
"captureGroups": 24,
"digest": "1d9ead4663799e035816ed06a17d37e9a98d0fd8090f9dea256ec6f019788648"
},
"rust-match-unwrap/src/repo.rs": {
"captureGroups": 9,
"digest": "09f0d2905c2fc05043fa767b277a23b787aad4b9c0fe22bc5569ca9660f7f18a"
},
"rust-match-unwrap/src/user.rs": {
"captureGroups": 9,
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
},
"rust-member-calls/src/main.rs": {
"captureGroups": 14,
"digest": "9de5ab8411808df29a924b2e854048c6babb1a09b996279aba5c3f7916d03ae0"
},
"rust-member-calls/src/user.rs": {
"captureGroups": 10,
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
},
"rust-method-chain-binding/src/main.rs": {
"captureGroups": 17,
"digest": "81b41ca25c672098dadbd7911c5a981706035b1bdca83eb2ca5f6bdbf759ab6b"
},
"rust-method-chain-binding/src/models.rs": {
"captureGroups": 34,
"digest": "cd836a2a9c15ab240961d2e15f192f7e33d65eb5ebf2e1a8af2f620a47fe66ae"
},
"rust-method-enrichment/src/lib.rs": {
"captureGroups": 37,
"digest": "a257cb6d2bf8f4ecc00d3c2880c5103d3d17ce9444f154be2fa283f5ce0a3e47"
},
"rust-method-enrichment/src/main.rs": {
"captureGroups": 18,
"digest": "3326eb4f82b1559b6afec497dc52cab734e6f3209501a4bd982bf5eab9ec6dba"
},
"rust-nullable-receiver/src/main.rs": {
"captureGroups": 37,
"digest": "283d8606eb837f2a9e5fdf95a30e3da5b73d4c14d74b94deb03e924dd2b2fde1"
},
"rust-nullable-receiver/src/repo.rs": {
"captureGroups": 10,
"digest": "24dcd450677a5885db0d93973e431e47b41e6866ad3fc7cb686f7a021000c17e"
},
"rust-nullable-receiver/src/user.rs": {
"captureGroups": 10,
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
},
"rust-option-receiver/src/main.rs": {
"captureGroups": 24,
"digest": "02298528e6fd679850ecaf6ed06017d29b7cac0f55ba2308692c87bd8904f040"
},
"rust-option-receiver/src/repo.rs": {
"captureGroups": 8,
"digest": "3d32ba72d93e2388f65228366c58276b6d90bb14f55351e4255e6d0067ad63dc"
},
"rust-option-receiver/src/user.rs": {
"captureGroups": 8,
"digest": "7e1cca87f7cec8a11d3f96a561639a65813d5dc202cf91f515ed65afc5b8df8b"
},
"rust-parent-resolution/src/lib.rs": {
"captureGroups": 3,
"digest": "141388068614e16d96f27cfdf18ac9001b9e202ce832fe10f38dab990637b3ab"
},
"rust-parent-resolution/src/serializable.rs": {
"captureGroups": 3,
"digest": "f35d44f44d81e3a0be40f68ba9dbd4bde6f01659fa15b6db34a458ad460f904e"
},
"rust-parent-resolution/src/user.rs": {
"captureGroups": 12,
"digest": "7c87a84a30e4de06e3bdc6f3adc8310aef09300bb035c77c0a3e6cf08c14c6ac"
},
"rust-receiver-resolution/src/main.rs": {
"captureGroups": 21,
"digest": "283b9d58dc76d8f48ab2fce4598f5173910f8c59f27c2aa9c1d80a3c86878be8"
},
"rust-receiver-resolution/src/repo.rs": {
"captureGroups": 10,
"digest": "24dcd450677a5885db0d93973e431e47b41e6866ad3fc7cb686f7a021000c17e"
},
"rust-receiver-resolution/src/user.rs": {
"captureGroups": 10,
"digest": "a5545efb669e40adf8428af48057cd93bd31c226f7d0952d4dae76e1ee5228f7"
},
"rust-reexport-chain/src/main.rs": {
"captureGroups": 12,
"digest": "d4ddd026d2159710adb889629e17104bb36d2855f15a37b39dadd1dc4cd6b993"
},
"rust-reexport-chain/src/models/handler.rs": {
"captureGroups": 11,
"digest": "04091a9f859230d384307beffab59331e960dad13953ef3919ab6b3cfbb585e7"
},
"rust-reexport-chain/src/models/mod.rs": {
"captureGroups": 3,
"digest": "f83985431e11f24a46d28f6c5256dd2e7b6d17a1079dd51bf8c4fe4f9087be11"
},
"rust-return-type-inference/src/main.rs": {
"captureGroups": 32,
"digest": "0f47930a3070f27b80e89065c29dfee3ce4c1923d5e66053553d7536bb521a23"
},
"rust-return-type-inference/src/models.rs": {
"captureGroups": 21,
"digest": "465517463a6f648955d92b6d179eb9f581a5431077c1eb22c4048d6283387458"
},
"rust-return-type/src/main.rs": {
"captureGroups": 11,
"digest": "2ab4961fb869cf40db597b422b24c7ba7a7b63d97487cea7013e065a32e497f3"
},
"rust-return-type/src/models.rs": {
"captureGroups": 17,
"digest": "0e3826200f2e6f5b948313369e85ee3a08f18bc8d51fbd1f7283b19a8e01aac8"
},
"rust-scoped-multi-file/src/main.rs": {
"captureGroups": 17,
"digest": "cbb0ad90a6a6ddcb71afb98a98a172bede7f5c06b5c1d31484a11315a264b311"
},
"rust-scoped-multi-file/src/models/mod.rs": {
"captureGroups": 5,
"digest": "7346b2cf62e4946b261ed0eb46f2623fb1882f46d832abdc2068012917c7b16a"
},
"rust-scoped-multi-file/src/models/repo.rs": {
"captureGroups": 18,
"digest": "5fdd3d9d0fa35089cfda53a3e84ac4b788c5dca97b7268c3f634d3bca5ba40d8"
},
"rust-scoped-multi-file/src/models/user.rs": {
"captureGroups": 18,
"digest": "cc1404f69ca2264fd6ae12aebbd5cc6844eccd68b11595ebe9c1e861d168a86b"
},
"rust-self-struct-literal/main.rs": {
"captureGroups": 11,
"digest": "3c0beb60f1487a63c60329e0843c1913b6a3854bb1ed3df84e4a07bc16dbd82d"
},
"rust-self-struct-literal/models.rs": {
"captureGroups": 31,
"digest": "91e3ba35c7dcf31ab8914f052bbec884d0b9f4f0ab991878b084fbf9e4fac192"
},
"rust-self-this-resolution/src/repo.rs": {
"captureGroups": 10,
"digest": "c182a765b5889287bf2fa6ca0367e33c051fdaa1a7058a6a4e7cafb355722384"
},
"rust-self-this-resolution/src/user.rs": {
"captureGroups": 16,
"digest": "9354dad2a222a66b46f00ceede6777ba8c0634294c41313f393bc9ebcbbb6aa7"
},
"rust-struct-destructuring/main.rs": {
"captureGroups": 15,
"digest": "6d96109296b7f5bd709e2704c14c332bfa0bca12453ae289b38a74da2c38124d"
},
"rust-struct-destructuring/point.rs": {
"captureGroups": 6,
"digest": "a00d6ac5f27b153c813b4d392c45ea47fc5d2a3fc8a9df1332e042a3593a48d5"
},
"rust-struct-destructuring/vec2.rs": {
"captureGroups": 9,
"digest": "ff25512c9a7c4c58d9e06e8ec55727ba13ca20361e7758b560bf0f53b6aa3a93"
},
"rust-struct-literal-inference/main.rs": {
"captureGroups": 19,
"digest": "e71269ff626cd0b0655591ee08e48d90e9a1421f3be8d9e8ac23262388a3f7ad"
},
"rust-struct-literal-inference/models.rs": {
"captureGroups": 26,
"digest": "363f8d0d3948d88b2b656a80db15d6c0c0c43e0f8ecc3e6a504cf8d8d7e6a020"
},
"rust-struct-literals/app.rs": {
"captureGroups": 12,
"digest": "46c3ea5d181fc61b6ea891adf439310ca9f941c0846a6a62c3a9ae9ac0c112f1"
},
"rust-struct-literals/user.rs": {
"captureGroups": 11,
"digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9"
},
"rust-traits/src/impls/button.rs": {
"captureGroups": 30,
"digest": "2b4523d8013330f59ce4bd44c7e8c4d8e185e691c5bef08fa8af20a86eed25de"
},
"rust-traits/src/main.rs": {
"captureGroups": 11,
"digest": "f1b9f72d74467be55a8b7679215b49bcabb4d0fced6080f752672070b32ed93d"
},
"rust-traits/src/traits/clickable.rs": {
"captureGroups": 3,
"digest": "3ed5b27c172d48f83929715ba92d1030a282f9f1e29ec2fcdd3d7e9efbc54a84"
},
"rust-traits/src/traits/drawable.rs": {
"captureGroups": 5,
"digest": "1dca39bbc7c1b1b66f1a34730b9a5b4dba04c54ee9d2688255e0fd4e6bc48499"
},
"rust-write-access/models.rs": {
"captureGroups": 9,
"digest": "660f755fd70cd1796f9da02ad7d65f599dea8029665ee45ecd18cd27919741f3"
},
"rust-write-access/service.rs": {
"captureGroups": 16,
"digest": "7edc72e15b18ec6cf0b48a592ccb3570f46ac25463640fc6a97d71fa1795b643"
},
"synthetic:dao-20": {
"captureGroups": 381,
"digest": "4b0262ff8d728d50f3f22772a1691001ee362936f7dd5395d458539cd6ca3411"
}
}

View file

@ -0,0 +1,57 @@
/**
* C# scope-capture O(n^2) regression tripwire.
*
* NOT gated behind GITNEXUS_BENCH and needs no compiled worker — it runs in
* normal CI and is the actual guard against an O(n^2) re-regression of
* `emitCsharpScopeCaptures` (the path PR #1918 made linear). It calls the
* hotpath directly on a ~400-entity generated source. The O(n) path (threading
* the tree-sitter query's captured node) does this in a few hundred ms; a
* findNodeAtRange-from-root re-regression would take many seconds at this size.
* The budget is a coarse tripwire (huge margin over the fixed path, far below a
* quadratic regression), not a microbenchmark — keep it generous so it never
* flakes on a loaded CI runner.
*
* Mirrors test/integration/python-scope-capture-tripwire.test.ts (issue #1848 /
* PR #1918).
*/
import { describe, it, expect } from 'vitest';
import { emitCsharpScopeCaptures } from '../../src/core/ingestion/languages/csharp/index.js';
describe('C# scope-capture O(n^2) regression tripwire', () => {
/**
* DAO-style source: a namespace + N classes, each carrying fields plus
* getter/setter methods. Reuses the synthetic DAO unit shape from
* bench/scope-capture/measure.mjs. Maximizes top-level children AND member
* matches, which is exactly the O(matches x rootChildren) shape the fix
* removed.
*/
function generateCsharpDaoSource(entityCount: number): string {
let src = 'namespace Generated;\n\n';
for (let i = 0; i < entityCount; i++) {
src +=
`public class Entity${i} {\n` +
` public long Id;\n public string Name;\n` +
` public long GetId() { return Id; }\n` +
` public void SetName(string v) { Name = v; }\n}\n\n`;
}
return src;
}
it('parses a 400-entity file in well under the O(n^2) tripwire budget', () => {
const ENTITY_COUNT = 400;
const BUDGET_MS = 10_000; // coarse: far over the fixed path, far under a quadratic regression
const src = generateCsharpDaoSource(ENTITY_COUNT);
emitCsharpScopeCaptures(src, 'tripwire-warmup.cs'); // warm up the parser/query JIT
const start = Date.now();
const matches = emitCsharpScopeCaptures(src, 'tripwire.cs');
const elapsedMs = Date.now() - start;
// Sanity: the captures are actually produced (each entity emits many capture
// groups), so a fast-but-empty result can't pass.
expect(matches.length).toBeGreaterThan(ENTITY_COUNT * 5);
// The actual regression guard: a re-regression to O(n^2) blows this budget.
expect(elapsedMs).toBeLessThan(BUDGET_MS);
}, 30_000);
});

View file

@ -0,0 +1,54 @@
/**
* PHP scope-capture O(n^2) regression tripwire.
*
* NOT gated behind GITNEXUS_BENCH and needs no compiled worker — it runs in
* normal CI and is the actual guard against an O(n^2) re-regression of
* `emitPhpScopeCaptures`. It calls the hotpath directly on a ~400-entity
* generated source. The O(n) path (threading the tree-sitter query's captured
* node) does this in a few hundred ms; the old findNodeAtRange-from-root
* behaviour took multiple seconds+ at this size. The budget is a coarse tripwire
* (huge margin over the fixed path, far below a quadratic regression), not a
* microbenchmark — keep it generous so it never flakes on a loaded CI runner.
*
* Mirrors test/integration/python-scope-capture-tripwire.test.ts (PR #1918).
*/
import { describe, it, expect } from 'vitest';
import { emitPhpScopeCaptures } from '../../src/core/ingestion/languages/php/index.js';
describe('PHP scope-capture O(n^2) regression tripwire', () => {
/**
* DAO-style source: top-level `<?php` header + N classes, each with fields and
* methods. Maximizes top-level children AND member matches, which is exactly
* the O(matches x rootChildren) shape the fix removed. Reuses the synthetic
* unit shape from bench/scope-capture/measure.mjs.
*/
function generatePhpDaoSource(entityCount: number): string {
let src = '<?php\n\n';
for (let i = 0; i < entityCount; i++) {
src +=
`class Entity${i} {\n` +
` public $id;\n public $name;\n` +
` function getId() { return $this->id; }\n` +
` function setName($v) { $this->name = $v; }\n}\n\n`;
}
return src;
}
it('parses a 400-entity file in well under the O(n^2) tripwire budget', () => {
const ENTITY_COUNT = 400;
const BUDGET_MS = 10_000; // coarse: far over the fixed path, far under a quadratic regression
const src = generatePhpDaoSource(ENTITY_COUNT);
emitPhpScopeCaptures(src, 'tripwire-warmup.php'); // warm up the parser/query JIT
const start = Date.now();
const matches = emitPhpScopeCaptures(src, 'tripwire.php');
const elapsedMs = Date.now() - start;
// Sanity: the captures are actually produced (each entity emits many capture
// groups), so a fast-but-empty result can't pass.
expect(matches.length).toBeGreaterThan(ENTITY_COUNT * 5);
// The actual regression guard: a re-regression to O(n^2) blows this budget.
expect(elapsedMs).toBeLessThan(BUDGET_MS);
}, 30_000);
});

View file

@ -0,0 +1,87 @@
/**
* Production-path regression guard for PR #1918 review finding P1.
*
* The Python file index (`getPythonFileIndex` in `import-target.ts`) is
* memoized on the `allFilePaths` Set identity via a WeakMap. The registry-
* primary path reaches it through `pythonScopeResolver.resolveImportTarget`
* (the orchestrator adapter) — NOT by calling `resolvePythonImportTarget`
* directly the way the unit parity test does. Before the fix, that adapter
* copied the set (`new Set(allFilePaths)`) on every import, handing a fresh
* WeakMap key per call so the index rebuilt every import (O(imports × files)).
*
* This test drives the adapter exactly as the orchestrator does and asserts the
* index is built ONCE across many imports on a stable set. It fails (build
* count == number of imports) if the per-import copy is reintroduced.
*/
import { describe, it, expect } from 'vitest';
import { pythonScopeResolver } from '../../src/core/ingestion/languages/python/scope-resolver.js';
import {
getPythonFileIndexBuildCount,
resetPythonFileIndexBuildCount,
} from '../../src/core/ingestion/languages/python/index-stats.js';
/**
* A synthetic workspace: a real package (`realpkg/__init__.py`, so the
* `hasRepoCandidate` gate passes) plus many unrelated modules. The imports
* below are multi-segment and miss every fast path, so each call reaches both
* `hasRepoCandidate` and `resolveAbsoluteFromFiles` — the two index consumers.
*/
function buildWorkspace(fileCount: number): Set<string> {
const files = new Set<string>();
for (let i = 0; i < fileCount; i++) {
files.add(`pkg/sub/mod${String(i).padStart(5, '0')}.py`);
}
files.add('realpkg/__init__.py');
files.add('realpkg/widget.py');
return files;
}
describe('Python import resolution — index reuse across imports (PR #1918 P1)', () => {
it('builds the file index once for many imports over a stable file set', () => {
const allFilePaths = buildWorkspace(300);
const fromFile = 'app/main.py';
const importCount = 300;
resetPythonFileIndexBuildCount();
for (let i = 0; i < importCount; i++) {
// Multi-segment, candidate-passing, suffix-miss → reaches the index.
pythonScopeResolver.resolveImportTarget(`realpkg.ghost${i}`, fromFile, allFilePaths);
}
// The whole point of PR #1918: O(imports + files), not O(imports × files).
// Pre-fix this was 300 (one rebuild per import via the adapter's Set copy).
expect(getPythonFileIndexBuildCount()).toBe(1);
});
it('rebuilds once per distinct file set (per-run isolation, no stale reuse)', () => {
const fromFile = 'app/main.py';
resetPythonFileIndexBuildCount();
const setA = buildWorkspace(50);
for (let i = 0; i < 20; i++) {
pythonScopeResolver.resolveImportTarget(`realpkg.ghost${i}`, fromFile, setA);
}
expect(getPythonFileIndexBuildCount()).toBe(1);
// A different Set instance is a different logical workspace → one more build.
const setB = buildWorkspace(50);
for (let i = 0; i < 20; i++) {
pythonScopeResolver.resolveImportTarget(`realpkg.ghost${i}`, fromFile, setB);
}
expect(getPythonFileIndexBuildCount()).toBe(2);
});
it('still resolves real imports correctly (the perf test is not vacuous)', () => {
const allFilePaths = buildWorkspace(20);
const fromFile = 'app/main.py';
// Suffix-fallback hit through the adapter: realpkg.widget → realpkg/widget.py.
expect(pythonScopeResolver.resolveImportTarget('realpkg.widget', fromFile, allFilePaths)).toBe(
'realpkg/widget.py',
);
// Gated-out / unresolvable import returns null.
expect(
pythonScopeResolver.resolveImportTarget('realpkg.ghost', fromFile, allFilePaths),
).toBeNull();
});
});

View file

@ -0,0 +1,72 @@
/**
* Python scope-capture O(n^2) regression tripwire.
*
* NOT gated behind GITNEXUS_BENCH and needs no compiled worker — it runs in
* normal CI and is the actual guard against an O(n^2) re-regression of
* `emitPythonScopeCaptures`. It calls the hotpath directly on a ~400-entity
* generated source. The O(n) path (threading the tree-sitter query's captured
* node) does this in a few hundred ms; the old findNodeAtRange-from-root
* behaviour took ~25s+ at this size. The budget is a coarse tripwire (huge
* margin over the fixed path, far below a quadratic regression), not a
* microbenchmark — keep it generous so it never flakes on a loaded CI runner.
*
* Mirrors test/integration/go-pipeline-benchmark.test.ts's "O(n^2) regression
* tripwire" suite (issue #1848).
*/
import { describe, it, expect } from 'vitest';
import { emitPythonScopeCaptures } from '../../src/core/ingestion/languages/python/index.js';
describe('Python scope-capture O(n^2) regression tripwire', () => {
/**
* DAO-style source: top-level imports + N classes (each with methods) + N
* module functions. Maximizes top-level children AND function matches, which
* is exactly the O(matches x rootChildren) shape the fix removed.
*/
function generatePythonDaoSource(entityCount: number): string {
const lines: string[] = [];
for (let i = 0; i < 12; i++) {
lines.push(`from pkg.mod${i} import alpha${i}, beta${i}, gamma${i} as g${i}`);
lines.push(`import top.level.module${i}`);
}
lines.push('');
for (let i = 0; i < entityCount; i++) {
const n = String(i).padStart(4, '0');
lines.push(
`class Entity${n}:`,
` def __init__(self, id: int, name: str):`,
` self.id = id`,
` self.name = name`,
` def get_id(self) -> int:`,
` return self.id`,
` def set_name(self, name: str) -> None:`,
` self.name = name`,
` @classmethod`,
` def make(cls, id: int):`,
` return cls(id, "x")`,
'',
`def build_entity${n}(id: int, name: str) -> Entity${n}:`,
` return Entity${n}(id, name)`,
'',
);
}
return lines.join('\n');
}
it('parses a 400-entity file in well under the O(n^2) tripwire budget', () => {
const ENTITY_COUNT = 400;
const BUDGET_MS = 10_000; // coarse: ~30x the fixed path, far under a quadratic regression
const src = generatePythonDaoSource(ENTITY_COUNT);
emitPythonScopeCaptures(src, 'tripwire-warmup.py'); // warm up the parser/query JIT
const start = Date.now();
const matches = emitPythonScopeCaptures(src, 'tripwire.py');
const elapsedMs = Date.now() - start;
// Sanity: the captures are actually produced (each entity emits many capture
// groups), so a fast-but-empty result can't pass.
expect(matches.length).toBeGreaterThan(ENTITY_COUNT * 10);
// The actual regression guard: a re-regression to O(n^2) blows this budget.
expect(elapsedMs).toBeLessThan(BUDGET_MS);
}, 30_000);
});

View file

@ -350,6 +350,89 @@ describe('C++ variadic call resolution', () => {
});
});
describe('C++ variadic packs and dependent-name resolution (#1894)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-variadic-dependent-resolution'),
() => {},
);
}, 60000);
it('keeps parameter-pack functions viable when call arity exceeds the fixed prefix', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'callVariadic' && c.target === 'logMany',
);
expect(calls).toHaveLength(1);
});
it('emits one fold-expression edge when the folded callee is unambiguous', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'logMany' && c.target === 'sink',
);
expect(calls).toHaveLength(1);
});
it('emits zero fold-expression edges when overload resolution remains ambiguous', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'foldAmbiguous' && c.target === 'ambiguous',
);
expect(calls).toHaveLength(0);
});
it('does not emit a concrete EXTENDS edge for a pack-expanded base', () => {
const extendsEdges = getRelationships(result, 'EXTENDS').filter(
(e) => e.source === 'Mix' && e.target === 'B',
);
expect(extendsEdges).toHaveLength(0);
});
it('does not bind unqualified member lookup through a pack-expanded dependent base', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'inherited',
);
expect(calls).toHaveLength(0);
});
it('preserves free helper calls inside a class with a pack-expanded dependent base', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'helper',
);
expect(calls).toHaveLength(1);
});
it('preserves using-declaration namespace helper calls inside a pack-base class', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'namespaceHelper',
);
expect(calls).toHaveLength(1);
});
it('resolves current-instantiation unqualified member calls', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'own',
);
expect(calls).toHaveLength(1);
});
it('keeps unknown-specialization member types unresolved', () => {
const calls = getRelationships(result, 'CALLS').filter(
(c) => c.source === 'run' && c.target === 'use',
);
expect(calls).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Local shadow: same-file definition takes priority over imported name
// ---------------------------------------------------------------------------

View file

@ -313,6 +313,11 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'g(1, 2) resolves to fixed-arity g(int, int), not g(int, ...)',
"h(1, 'a') resolves to h(int, double), not h(int, ...)",
'k(1, 2, 3) keeps the ellipsis overload viable when it is the only match',
// Pack-expanded dependent bases (`struct Mix : B...`) are suppressed
// at C++ scope-capture time in the registry-primary path. The legacy
// DAG still sees same-file class-owned methods by simple name and
// over-emits `Mix::run -> B::inherited`.
'does not bind unqualified member lookup through a pack-expanded dependent base',
// User-defined conversion ranking (#1631) builds on the C++
// conversion-rank hook and the registry-primary C++ owner sidecars.
// Legacy DAG has no user-defined-conversion sidecar or ranking path.

View file

@ -0,0 +1,54 @@
/**
* Ruby scope-capture O(n^2) regression tripwire.
*
* NOT gated behind GITNEXUS_BENCH and needs no compiled worker — it runs in
* normal CI and is the actual guard against an O(n^2) re-regression of
* `emitRubyScopeCaptures`. It calls the hotpath directly on a ~400-entity
* generated source. The O(n) path (PR #1918, threading the tree-sitter query's
* captured node) does this in a few hundred ms; the old
* findNodeAtRange-from-root behaviour scaled quadratically at this size. The
* budget is a coarse tripwire (huge margin over the fixed path, far below a
* quadratic regression), not a microbenchmark — keep it generous so it never
* flakes on a loaded CI runner.
*
* Mirrors test/integration/python-scope-capture-tripwire.test.ts (issue #1848).
*/
import { describe, it, expect } from 'vitest';
import { emitRubyScopeCaptures } from '../../src/core/ingestion/languages/ruby/index.js';
describe('Ruby scope-capture O(n^2) regression tripwire', () => {
/**
* DAO-style source: N classes, each with two methods. Maximizes top-level
* children AND method matches, which is exactly the O(matches x rootChildren)
* shape the fix removed. Mirrors the `ruby` unit shape in
* bench/scope-capture/measure.mjs.
*/
function generateRubyDaoSource(entityCount: number): string {
let src = '';
for (let i = 0; i < entityCount; i++) {
src +=
`class Entity${i}\n` +
` def get_id\n @id\n end\n` +
` def set_name(v)\n @name = v\n end\nend\n\n`;
}
return src;
}
it('parses a 400-entity file in well under the O(n^2) tripwire budget', () => {
const ENTITY_COUNT = 400;
const BUDGET_MS = 10_000; // coarse: many x the fixed path, far under a quadratic regression
const src = generateRubyDaoSource(ENTITY_COUNT);
emitRubyScopeCaptures(src, 'tripwire-warmup.rb'); // warm up the parser/query JIT
const start = Date.now();
const matches = emitRubyScopeCaptures(src, 'tripwire.rb');
const elapsedMs = Date.now() - start;
// Sanity: the captures are actually produced (each entity emits many capture
// groups), so a fast-but-empty result can't pass.
expect(matches.length).toBeGreaterThan(ENTITY_COUNT * 5);
// The actual regression guard: a re-regression to O(n^2) blows this budget.
expect(elapsedMs).toBeLessThan(BUDGET_MS);
}, 30_000);
});

View file

@ -0,0 +1,54 @@
/**
* Rust scope-capture O(n^2) regression tripwire (PR #1918 follow-up).
*
* NOT gated behind GITNEXUS_BENCH and needs no compiled worker — it runs in
* normal CI and is the actual guard against an O(n^2) re-regression of
* `emitRustScopeCaptures`. It calls the hotpath directly on a ~400-entity
* generated source. The O(n) path (threading the tree-sitter query's captured
* node) does this in a few hundred ms; the old findNodeAtRange-from-root
* behaviour took many seconds at this size. The budget is a coarse tripwire
* (huge margin over the fixed path, far below a quadratic regression), not a
* microbenchmark — keep it generous so it never flakes on a loaded CI runner.
*
* Mirrors test/integration/python-scope-capture-tripwire.test.ts (issue #1848).
*/
import { describe, it, expect } from 'vitest';
import { emitRustScopeCaptures } from '../../src/core/ingestion/languages/rust/index.js';
describe('Rust scope-capture O(n^2) regression tripwire', () => {
/**
* DAO-style source: N structs each with an `impl` block of methods. Maximizes
* top-level children AND method matches — exactly the
* O(matches x rootChildren) shape the fix removed. Mirrors the rust `unit`
* shape in bench/scope-capture/measure.mjs.
*/
function generateRustDaoSource(entityCount: number): string {
let src = '';
for (let i = 0; i < entityCount; i++) {
src +=
`struct Entity${i} {\n id: i64,\n name: String,\n}\n\n` +
`impl Entity${i} {\n` +
` fn get_id(&self) -> i64 { self.id }\n` +
` fn set_name(&mut self, v: String) { self.name = v; }\n}\n\n`;
}
return src;
}
it('parses a 400-entity file in well under the O(n^2) tripwire budget', () => {
const ENTITY_COUNT = 400;
const BUDGET_MS = 10_000; // coarse: many x the fixed path, far under a quadratic regression
const src = generateRustDaoSource(ENTITY_COUNT);
emitRustScopeCaptures(src, 'tripwire-warmup.rs'); // warm up the parser/query JIT
const start = Date.now();
const matches = emitRustScopeCaptures(src, 'tripwire.rs');
const elapsedMs = Date.now() - start;
// Sanity: the captures are actually produced (each entity emits many capture
// groups), so a fast-but-empty result can't pass.
expect(matches.length).toBeGreaterThan(ENTITY_COUNT * 5);
// The actual regression guard: a re-regression to O(n^2) blows this budget.
expect(elapsedMs).toBeLessThan(BUDGET_MS);
}, 30_000);
});

View file

@ -0,0 +1,233 @@
/**
* Golden capture-parity test for `emitCsharpScopeCaptures` (PR #1918 follow-up).
*
* Pins the exact capture output of `emitCsharpScopeCaptures` across the whole
* `test/fixtures/lang-resolution/csharp-*` corpus plus a synthetic generated-DAO
* source, so any future drift in the C# scope-capture path fails CI rather than
* only being caught by a coarse perf tripwire, the bench fingerprint CI job, or
* pipeline-level resolver tests.
*
* This is a FORWARD-DRIFT guard: it locks in the current (post-#1918, verified)
* linear output as the baseline. It does not independently re-prove the original
* pre-optimization parity — that was established during PR #1918.
*
* Regenerate the golden intentionally with `UPDATE_GOLDEN=1` in the environment.
*
* Per fixture the snapshot stores `{ captureGroups, digest }`:
* - captureGroups: number of capture matches (makes a count change legible)
* - digest: sha256 of a match-grouped, order-sensitive (emission-order)
* canonicalization (see canonicalize* below). Order-sensitivity is safe
* because emitCsharpScopeCaptures output is deterministic, and it makes the
* digest a true byte-identical guard (a reordering refactor is real drift).
* Nothing path/time/id-dependent leaks in.
*
* Pattern: mirrors test/unit/scope-resolution/go/go-captures-golden.test.ts.
*/
import { describe, it, expect } from 'vitest';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { emitCsharpScopeCaptures } from '../../../../src/core/ingestion/languages/csharp/index.js';
import type { CaptureMatch } from 'gitnexus-shared';
// This test lives at test/unit/scope-resolution/csharp/, so fixtures are THREE
// levels up (unlike pipeline-graph-golden.test.ts at test/integration/).
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'lang-resolution');
const GOLDEN_DIR = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'csharp-captures-golden');
const GOLDEN_FILE = path.join(GOLDEN_DIR, 'expected-captures.json');
const UPDATE = process.env.UPDATE_GOLDEN === '1';
interface FixtureSnapshot {
captureGroups: number;
digest: string;
}
type Snapshot = Record<string, FixtureSnapshot>;
/**
* Canonicalize ONE match. A CaptureMatch is a Record<tag, Capture> (multiple
* captures per match), so we group by match to preserve match identity:
* build one `tag|text|startLine:startCol-endLine:endCol` string per capture,
* sort them within the match, and join. We deliberately do NOT flatten every
* capture into one global list — that would lose match boundaries.
*/
function canonicalizeMatch(match: CaptureMatch): string {
const parts: string[] = [];
for (const tag of Object.keys(match)) {
const cap = match[tag]!;
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
/** Order-sensitive (emission-order) digest of a full capture result (match-grouped). */
function digestCaptures(matches: readonly CaptureMatch[]): string {
// No cross-match sort: the digest reflects emission order so a reordering
// refactor surfaces as drift. Within-match key order IS normalized
// (canonicalizeMatch sorts), since a CaptureMatch is an unordered Record.
const matchStrings = matches.map(canonicalizeMatch);
return crypto.createHash('sha256').update(matchStrings.join('\n')).digest('hex');
}
function snapshotOf(src: string, filePath: string): FixtureSnapshot {
const matches = emitCsharpScopeCaptures(src, filePath);
return { captureGroups: matches.length, digest: digestCaptures(matches) };
}
/** All `.cs` files under `lang-resolution/csharp-*`, as sorted repo-relative-ish keys. */
function collectCsharpFixtures(): { key: string; absPath: string }[] {
const out: { key: string; absPath: string }[] = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('csharp-')) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop()!;
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (c.name.endsWith('.cs')) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
/**
* Small deterministic generated-DAO source — the #1918 shape at correctness
* scale. Reuses the synthetic DAO unit shape from bench/scope-capture/measure.mjs:
* a namespace with N classes, each carrying fields plus getter/setter methods.
*/
function generateDao(entityCount: number): string {
let src = 'namespace Generated;\n\n';
for (let i = 0; i < entityCount; i++) {
src +=
`public class Entity${i} {\n` +
` public long Id;\n public string Name;\n` +
` public long GetId() { return Id; }\n` +
` public void SetName(string v) { Name = v; }\n}\n\n`;
}
return src;
}
function buildSnapshot(): Snapshot {
const snap: Snapshot = {};
for (const { key, absPath } of collectCsharpFixtures()) {
snap[key] = snapshotOf(fs.readFileSync(absPath, 'utf8'), absPath);
}
snap['synthetic:dao-20'] = snapshotOf(generateDao(20), 'zz_generated_dao.cs');
// Stable key order for deterministic JSON serialization.
return Object.fromEntries(
Object.keys(snap)
.sort()
.map((k) => [k, snap[k]!]),
);
}
function formatGolden(snap: Snapshot): string {
return JSON.stringify(snap, null, 2) + '\n';
}
/**
* Pure decision for what the golden test should do — extracted so the
* fail-on-missing-in-CI rule is unit-testable without touching the filesystem
* (and can never corrupt the committed golden). A missing golden must NOT
* self-heal in CI; locally it regenerates as a first-run convenience.
*/
type GoldenAction = 'regenerate' | 'compare' | 'fail';
function resolveGoldenAction(opts: {
update: boolean;
exists: boolean;
isCI: boolean;
}): GoldenAction {
if (opts.update) return 'regenerate';
if (!opts.exists) return opts.isCI ? 'fail' : 'regenerate';
return 'compare';
}
describe('C# scope captures — golden parity', () => {
it('matches the committed golden snapshot across all csharp-* fixtures + DAO shape', () => {
const snapshot = buildSnapshot();
// Read the golden once (no existsSync-then-use, which is a TOCTOU race):
// ENOENT means the golden is missing; reuse `existing` for the compare path.
let existing: string | undefined;
try {
existing = fs.readFileSync(GOLDEN_FILE, 'utf8');
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
}
const action = resolveGoldenAction({
update: UPDATE,
exists: existing !== undefined,
isCI: !!process.env.CI, // truthy check: fires on any CI runner, not just CI==='true'
});
if (action === 'fail') {
throw new Error(
`[csharp-captures-golden] golden file missing at ${GOLDEN_FILE} in CI. A missing golden must ` +
`not self-heal in CI — regenerate it locally with UPDATE_GOLDEN=1 and commit it.`,
);
}
if (action === 'regenerate') {
fs.mkdirSync(GOLDEN_DIR, { recursive: true });
fs.writeFileSync(GOLDEN_FILE, formatGolden(snapshot), 'utf8');
console.log(
`[csharp-captures-golden] ${UPDATE ? 'Regenerated' : 'Created'} golden at ${GOLDEN_FILE}`,
);
return;
}
const expected: Snapshot = JSON.parse(existing!);
expect(
snapshot,
'emitCsharpScopeCaptures output drifted from the committed golden. If this drift is intentional ' +
'(or the digest scheme changed), regenerate with ' +
'UPDATE_GOLDEN=1 npx vitest run test/unit/scope-resolution/csharp/csharp-captures-golden.test.ts',
).toEqual(expected);
});
// The fail-on-missing-in-CI rule, asserted purely (no filesystem mutation).
it.each([
{ update: true, exists: false, isCI: true, expected: 'regenerate' },
{ update: false, exists: false, isCI: true, expected: 'fail' },
{ update: false, exists: false, isCI: false, expected: 'regenerate' },
{ update: false, exists: true, isCI: true, expected: 'compare' },
{ update: false, exists: true, isCI: false, expected: 'compare' },
])(
'resolveGoldenAction($update,$exists,$isCI) -> $expected',
({ update, exists, isCI, expected }) => {
expect(resolveGoldenAction({ update, exists, isCI })).toBe(expected);
},
);
it('produces a deterministic digest across repeated runs', () => {
const src = generateDao(8);
expect(digestCaptures(emitCsharpScopeCaptures(src, 'a.cs'))).toBe(
digestCaptures(emitCsharpScopeCaptures(src, 'a.cs')),
);
});
it('digest is sensitive to capture-match emission order', () => {
const matches = emitCsharpScopeCaptures(generateDao(6), 'a.cs');
expect(matches.length).toBeGreaterThan(1);
const reversed = [...matches].reverse();
// Reordering the emission changes the digest — the true byte-identical guard.
expect(digestCaptures(reversed)).not.toBe(digestCaptures(matches));
});
it('records a capture-group count for every fixture and the DAO shape', () => {
const snapshot = buildSnapshot();
const fixtureKeys = collectCsharpFixtures().map((f) => f.key);
// Every collected fixture is present in the snapshot.
for (const k of fixtureKeys) expect(snapshot[k]).toBeDefined();
// The DAO shape (which has symbols) yields a non-empty capture set.
expect(snapshot['synthetic:dao-20']!.captureGroups).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,228 @@
/**
* Golden capture-parity test for `emitPhpScopeCaptures` (PR #1918 follow-up).
*
* Pins the exact capture output of `emitPhpScopeCaptures` across the whole
* `test/fixtures/lang-resolution/php-*` corpus plus a synthetic generated-DAO
* source, so any future drift in the PHP scope-capture path fails CI rather than
* only being caught by a coarse perf tripwire or pipeline-level resolver tests.
*
* This is a FORWARD-DRIFT guard: it locks in the current (post-#1918, verified)
* output as the baseline. It does not independently re-prove the original
* pre-fix parity — that was established during PR #1918.
*
* Regenerate the golden intentionally with `UPDATE_GOLDEN=1` in the environment.
*
* Per fixture the snapshot stores `{ captureGroups, digest }`:
* - captureGroups: number of capture matches (makes a count change legible)
* - digest: sha256 of a match-grouped, order-sensitive (emission-order)
* canonicalization (see canonicalize* below). Order-sensitivity is safe
* because emitPhpScopeCaptures output is deterministic, and it makes the
* digest a true byte-identical guard (a reordering refactor is real drift).
* Nothing path/time/id-dependent leaks in.
*
* Pattern: mirrors test/unit/scope-resolution/go/go-captures-golden.test.ts.
*/
import { describe, it, expect } from 'vitest';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { emitPhpScopeCaptures } from '../../../../src/core/ingestion/languages/php/index.js';
import type { CaptureMatch } from 'gitnexus-shared';
// This test lives at test/unit/scope-resolution/php/, so fixtures are THREE
// levels up (unlike pipeline-graph-golden.test.ts at test/integration/).
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'lang-resolution');
const GOLDEN_DIR = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'php-captures-golden');
const GOLDEN_FILE = path.join(GOLDEN_DIR, 'expected-captures.json');
const UPDATE = process.env.UPDATE_GOLDEN === '1';
interface FixtureSnapshot {
captureGroups: number;
digest: string;
}
type Snapshot = Record<string, FixtureSnapshot>;
/**
* Canonicalize ONE match. A CaptureMatch is a Record<tag, Capture> (multiple
* captures per match), so we group by match to preserve match identity:
* build one `tag|text|startLine:startCol-endLine:endCol` string per capture,
* sort them within the match, and join. We deliberately do NOT flatten every
* capture into one global list — that would lose match boundaries.
*/
function canonicalizeMatch(match: CaptureMatch): string {
const parts: string[] = [];
for (const tag of Object.keys(match)) {
const cap = match[tag]!;
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
/** Order-sensitive (emission-order) digest of a full capture result (match-grouped). */
function digestCaptures(matches: readonly CaptureMatch[]): string {
// No cross-match sort: the digest reflects emission order so a reordering
// refactor surfaces as drift. Within-match key order IS normalized
// (canonicalizeMatch sorts), since a CaptureMatch is an unordered Record.
const matchStrings = matches.map(canonicalizeMatch);
return crypto.createHash('sha256').update(matchStrings.join('\n')).digest('hex');
}
function snapshotOf(src: string, filePath: string): FixtureSnapshot {
const matches = emitPhpScopeCaptures(src, filePath);
return { captureGroups: matches.length, digest: digestCaptures(matches) };
}
/** All `.php` files under `lang-resolution/php-*`, as sorted repo-relative-ish keys. */
function collectPhpFixtures(): { key: string; absPath: string }[] {
const out: { key: string; absPath: string }[] = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('php-')) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop()!;
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (c.name.endsWith('.php')) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
/** Small deterministic generated-DAO source — the #1848 shape at correctness scale. */
function generateDao(entityCount: number): string {
let src = '<?php\n\n';
for (let i = 0; i < entityCount; i++) {
src +=
`class Entity${i} {\n` +
` public $id;\n public $name;\n` +
` function getId() { return $this->id; }\n` +
` function setName($v) { $this->name = $v; }\n}\n\n`;
}
return src;
}
function buildSnapshot(): Snapshot {
const snap: Snapshot = {};
for (const { key, absPath } of collectPhpFixtures()) {
snap[key] = snapshotOf(fs.readFileSync(absPath, 'utf8'), absPath);
}
snap['synthetic:dao-20'] = snapshotOf(generateDao(20), 'zz_generated_dao.php');
// Stable key order for deterministic JSON serialization.
return Object.fromEntries(
Object.keys(snap)
.sort()
.map((k) => [k, snap[k]!]),
);
}
function formatGolden(snap: Snapshot): string {
return JSON.stringify(snap, null, 2) + '\n';
}
/**
* Pure decision for what the golden test should do — extracted so the
* fail-on-missing-in-CI rule is unit-testable without touching the filesystem
* (and can never corrupt the committed golden). A missing golden must NOT
* self-heal in CI; locally it regenerates as a first-run convenience.
*/
type GoldenAction = 'regenerate' | 'compare' | 'fail';
function resolveGoldenAction(opts: {
update: boolean;
exists: boolean;
isCI: boolean;
}): GoldenAction {
if (opts.update) return 'regenerate';
if (!opts.exists) return opts.isCI ? 'fail' : 'regenerate';
return 'compare';
}
describe('PHP scope captures — golden parity', () => {
it('matches the committed golden snapshot across all php-* fixtures + DAO shape', () => {
const snapshot = buildSnapshot();
// Read the golden once (no existsSync-then-use, which is a TOCTOU race):
// ENOENT means the golden is missing; reuse `existing` for the compare path.
let existing: string | undefined;
try {
existing = fs.readFileSync(GOLDEN_FILE, 'utf8');
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
}
const action = resolveGoldenAction({
update: UPDATE,
exists: existing !== undefined,
isCI: !!process.env.CI, // truthy check: fires on any CI runner, not just CI==='true'
});
if (action === 'fail') {
throw new Error(
`[php-captures-golden] golden file missing at ${GOLDEN_FILE} in CI. A missing golden must ` +
`not self-heal in CI — regenerate it locally with UPDATE_GOLDEN=1 and commit it.`,
);
}
if (action === 'regenerate') {
fs.mkdirSync(GOLDEN_DIR, { recursive: true });
fs.writeFileSync(GOLDEN_FILE, formatGolden(snapshot), 'utf8');
console.log(
`[php-captures-golden] ${UPDATE ? 'Regenerated' : 'Created'} golden at ${GOLDEN_FILE}`,
);
return;
}
const expected: Snapshot = JSON.parse(existing!);
expect(
snapshot,
'emitPhpScopeCaptures output drifted from the committed golden. If this drift is intentional ' +
'(or the digest scheme changed), regenerate with ' +
'UPDATE_GOLDEN=1 npx vitest run test/unit/scope-resolution/php/php-captures-golden.test.ts',
).toEqual(expected);
});
// The fail-on-missing-in-CI rule, asserted purely (no filesystem mutation).
it.each([
{ update: true, exists: false, isCI: true, expected: 'regenerate' },
{ update: false, exists: false, isCI: true, expected: 'fail' },
{ update: false, exists: false, isCI: false, expected: 'regenerate' },
{ update: false, exists: true, isCI: true, expected: 'compare' },
{ update: false, exists: true, isCI: false, expected: 'compare' },
])(
'resolveGoldenAction($update,$exists,$isCI) -> $expected',
({ update, exists, isCI, expected }) => {
expect(resolveGoldenAction({ update, exists, isCI })).toBe(expected);
},
);
it('produces a deterministic digest across repeated runs', () => {
const src = generateDao(8);
expect(digestCaptures(emitPhpScopeCaptures(src, 'a.php'))).toBe(
digestCaptures(emitPhpScopeCaptures(src, 'a.php')),
);
});
it('digest is sensitive to capture-match emission order', () => {
const matches = emitPhpScopeCaptures(generateDao(6), 'a.php');
expect(matches.length).toBeGreaterThan(1);
const reversed = [...matches].reverse();
// Reordering the emission changes the digest — the true byte-identical guard.
expect(digestCaptures(reversed)).not.toBe(digestCaptures(matches));
});
it('records a capture-group count for every fixture and the DAO shape', () => {
const snapshot = buildSnapshot();
const fixtureKeys = collectPhpFixtures().map((f) => f.key);
// Every collected fixture is present in the snapshot.
for (const k of fixtureKeys) expect(snapshot[k]).toBeDefined();
// The DAO shape (which has symbols) yields a non-empty capture set.
expect(snapshot['synthetic:dao-20']!.captureGroups).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,188 @@
/**
* Golden capture-parity test for `emitPythonScopeCaptures`.
*
* Pins the exact capture output of `emitPythonScopeCaptures` across the whole
* `test/fixtures/lang-resolution/python-*` corpus plus a synthetic DAO-style
* source, so any future drift in the Python scope-capture path fails CI rather
* than only being caught by a coarse perf tripwire or pipeline-level resolver
* tests.
*
* This is the correctness anchor for the O(n^2) -> O(n) rewrite of
* emitPythonScopeCaptures (threading the tree-sitter query's captured node
* instead of re-deriving it with findNodeAtRange from the tree root — the same
* fix shipped for Go in #1848). It is a FORWARD-DRIFT guard: it locks in the
* current verified output as the baseline.
*
* Regenerate the golden intentionally with `UPDATE_GOLDEN=1` in the environment.
*
* Per fixture the snapshot stores `{ captureGroups, digest }`:
* - captureGroups: number of capture matches (makes a count change legible)
* - digest: sha256 of a match-grouped, order-independent canonicalization.
* Nothing path/time/id-dependent leaks in.
*
* Pattern: mirrors test/unit/scope-resolution/go/go-captures-golden.test.ts.
*/
import { describe, it, expect } from 'vitest';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { emitPythonScopeCaptures } from '../../../../src/core/ingestion/languages/python/index.js';
import type { CaptureMatch } from 'gitnexus-shared';
// This test lives at test/unit/scope-resolution/python/, so fixtures are FOUR
// levels up.
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'lang-resolution');
const GOLDEN_DIR = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'python-captures-golden');
const GOLDEN_FILE = path.join(GOLDEN_DIR, 'expected-captures.json');
const UPDATE = process.env.UPDATE_GOLDEN === '1';
interface FixtureSnapshot {
captureGroups: number;
digest: string;
}
type Snapshot = Record<string, FixtureSnapshot>;
/**
* Canonicalize ONE match. A CaptureMatch is a Record<tag, Capture> (multiple
* captures per match), so we group by match to preserve match identity:
* build one `tag|text|startLine:startCol-endLine:endCol` string per capture,
* sort them within the match, and join. We deliberately do NOT flatten every
* capture into one global list — that would lose match boundaries.
*/
function canonicalizeMatch(match: CaptureMatch): string {
const parts: string[] = [];
for (const tag of Object.keys(match)) {
const cap = match[tag]!;
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
/** Order-independent digest of a full capture result (match-grouped). */
function digestCaptures(matches: readonly CaptureMatch[]): string {
const matchStrings = matches.map(canonicalizeMatch).sort();
return crypto.createHash('sha256').update(matchStrings.join('\n')).digest('hex');
}
function snapshotOf(src: string, filePath: string): FixtureSnapshot {
const matches = emitPythonScopeCaptures(src, filePath);
return { captureGroups: matches.length, digest: digestCaptures(matches) };
}
/** All `.py` files under `lang-resolution/python-*`, as sorted repo-relative-ish keys. */
function collectPythonFixtures(): { key: string; absPath: string }[] {
const out: { key: string; absPath: string }[] = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('python-')) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop()!;
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (c.name.endsWith('.py')) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
/**
* Small deterministic generated-DAO source — exercises imports, class scopes,
* methods (@scope.function / @declaration.function / receiver binding), and
* module functions, the shape that stresses the scope-capture path at scale.
*/
function generateDao(entityCount: number): string {
const lines: string[] = [];
for (let i = 0; i < 4; i++) {
lines.push(`from pkg.mod${i} import alpha${i}, beta${i} as b${i}`);
lines.push(`import top.level.module${i}`);
}
lines.push('');
for (let i = 0; i < entityCount; i++) {
const n = String(i).padStart(4, '0');
lines.push(
`class Entity${n}:`,
` def __init__(self, id: int, name: str):`,
` self.id = id`,
` self.name = name`,
` def get_id(self) -> int:`,
` return self.id`,
` @classmethod`,
` def make(cls, id: int):`,
` return cls(id, "x")`,
'',
`def build_entity${n}(id: int) -> Entity${n}:`,
` return Entity${n}(id, "x")`,
'',
);
}
return lines.join('\n');
}
function buildSnapshot(): Snapshot {
const snap: Snapshot = {};
for (const { key, absPath } of collectPythonFixtures()) {
snap[key] = snapshotOf(fs.readFileSync(absPath, 'utf8'), absPath);
}
snap['synthetic:dao-20'] = snapshotOf(generateDao(20), 'zz_generated_dao.py');
// Stable key order for deterministic JSON serialization.
return Object.fromEntries(
Object.keys(snap)
.sort()
.map((k) => [k, snap[k]!]),
);
}
function formatGolden(snap: Snapshot): string {
return JSON.stringify(snap, null, 2) + '\n';
}
describe('Python scope captures — golden parity', () => {
it('matches the committed golden snapshot across all python-* fixtures + DAO shape', () => {
const snapshot = buildSnapshot();
if (UPDATE || !fs.existsSync(GOLDEN_FILE)) {
fs.mkdirSync(GOLDEN_DIR, { recursive: true });
fs.writeFileSync(GOLDEN_FILE, formatGolden(snapshot), 'utf8');
console.log(
`[python-captures-golden] ${UPDATE ? 'Regenerated' : 'Created'} golden at ${GOLDEN_FILE}`,
);
return;
}
const expected: Snapshot = JSON.parse(fs.readFileSync(GOLDEN_FILE, 'utf8'));
expect(
snapshot,
'emitPythonScopeCaptures output drifted from the committed golden. If this drift is ' +
'intentional, regenerate with UPDATE_GOLDEN=1 npx vitest run ' +
'test/unit/scope-resolution/python/python-captures-golden.test.ts',
).toEqual(expected);
});
it('produces a deterministic digest across repeated runs', () => {
const src = generateDao(8);
expect(digestCaptures(emitPythonScopeCaptures(src, 'a.py'))).toBe(
digestCaptures(emitPythonScopeCaptures(src, 'a.py')),
);
});
it('digest is independent of capture-match array order', () => {
const matches = emitPythonScopeCaptures(generateDao(6), 'a.py');
const reversed = [...matches].reverse();
expect(digestCaptures(reversed)).toBe(digestCaptures(matches));
});
it('records a capture-group count for every fixture and the DAO shape', () => {
const snapshot = buildSnapshot();
const fixtureKeys = collectPythonFixtures().map((f) => f.key);
for (const k of fixtureKeys) expect(snapshot[k]).toBeDefined();
expect(snapshot['synthetic:dao-20']!.captureGroups).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,153 @@
/**
* Parity guard for the memoized file index in `resolvePythonImportTarget`
* (import-target.ts).
*
* The index replaces two per-import O(files) scans (the suffix match in
* `resolveAbsoluteFromFiles` and the package-existence gate in
* `hasRepoCandidate`) with O(1)/O(bucket) lookups. It MUST reproduce the exact
* resolution result — in particular the deterministic tie-break
* (fewest-segments, then lexicographic) and the false-positive gating that the
* import-target.ts comments call out. These cases pin those semantics so an
* index regression fails CI rather than silently changing resolved edges.
*/
import { describe, it, expect } from 'vitest';
import { resolvePythonImportTarget } from '../../../../src/core/ingestion/languages/python/index.js';
import type { ParsedImport } from 'gitnexus-shared';
function mkImport(targetRaw: string): ParsedImport {
return { kind: 'absolute', targetRaw, isRelative: false, names: [] } as unknown as ParsedImport;
}
function resolve(fromFile: string, files: string[], targetRaw: string): string | null {
return resolvePythonImportTarget(mkImport(targetRaw), {
fromFile,
allFilePaths: new Set(files),
});
}
describe('resolvePythonImportTarget — index parity', () => {
it('direct workspace-root hit wins', () => {
expect(
resolve('app/main.py', ['services/sync.py', 'services/__init__.py'], 'services.sync'),
).toBe('services/sync.py');
});
it('ancestor walk resolves nested namespace packages', () => {
expect(resolve('backend/routers/cron.py', ['backend/services/sync.py'], 'services.sync')).toBe(
'backend/services/sync.py',
);
});
it('suffix fallback resolves a nested vendored layout', () => {
expect(resolve('app/main.py', ['pkg/__init__.py', 'vendor/pkg/thing.py'], 'pkg.thing')).toBe(
'vendor/pkg/thing.py',
);
});
it('suffix tie-break prefers fewest path segments', () => {
expect(
resolve(
'app/main.py',
['pkg/__init__.py', 'a/pkg/models.py', 'b/c/pkg/models.py'],
'pkg.models',
),
).toBe('a/pkg/models.py');
});
it('suffix tie-break at equal depth is lexicographic', () => {
expect(
resolve(
'app/main.py',
['pkg/__init__.py', 'z/pkg/models.py', 'a/pkg/models.py'],
'pkg.models',
),
).toBe('a/pkg/models.py');
});
it('external dotted import is gated out by hasRepoCandidate (django.apps guard)', () => {
expect(resolve('app/main.py', ['accounts/apps.py'], 'django.apps')).toBeNull();
});
it('does not suffix-match a different package basename (accounts.models vs billing/models.py)', () => {
expect(
resolve('app/main.py', ['accounts/__init__.py', 'billing/models.py'], 'accounts.models'),
).toBeNull();
});
it('candidate exists but no concrete file resolves to null', () => {
expect(resolve('app/main.py', ['pkg/__init__.py'], 'pkg.ghost')).toBeNull();
});
it('package __init__ suffix resolves', () => {
expect(
resolve('app/main.py', ['pkg/__init__.py', 'x/pkg/subpkg/__init__.py'], 'pkg.subpkg'),
).toBe('x/pkg/subpkg/__init__.py');
});
it('the index is reused across imports on the same file set (no stale results)', () => {
const files = ['pkg/__init__.py', 'a/pkg/models.py', 'vendor/pkg/thing.py'];
const ctx = { fromFile: 'app/main.py', allFilePaths: new Set(files) };
expect(resolvePythonImportTarget(mkImport('pkg.models'), ctx)).toBe('a/pkg/models.py');
expect(resolvePythonImportTarget(mkImport('pkg.thing'), ctx)).toBe('vendor/pkg/thing.py');
expect(resolvePythonImportTarget(mkImport('pkg.ghost'), ctx)).toBeNull();
});
it('resolves a nested package via the parent-keyed __init__ bucket (PR #1918 P2b)', () => {
// `mypkg` is a candidate (root package), but the real target is nested under
// vendor/. `noise/sub/__init__.py` shares the parent-bucket key (`sub`) yet
// is filtered out by the full-suffix confirm — proving the parent bucket is
// a candidate set, not the answer, and that the result matches the old scan.
const files = ['mypkg/__init__.py', 'vendor/mypkg/sub/__init__.py', 'noise/sub/__init__.py'];
expect(resolve('app/main.py', files, 'mypkg.sub')).toBe('vendor/mypkg/sub/__init__.py');
});
it('resolves an explicit pkg.__init__ import via the module lookup', () => {
// `from pkg.__init__ import x` targets the package init module directly;
// it must still resolve (it goes through the `<lastSeg>.py` = `__init__.py`
// bucket, not the parent-keyed package bucket).
expect(resolve('app/main.py', ['pkg/__init__.py', 'pkg/widget.py'], 'pkg.__init__')).toBe(
'pkg/__init__.py',
);
});
it('reproduces old startsWith gating for absolute paths (PR #1918 P3a)', () => {
// Absolute file set. hasRepoCandidate must NOT gate-pass `pkg` off
// `/repo/pkg/__init__.py` the way the first #1918 index did (its prefix set
// dropped the leading slash). The old full-scan gate did
// `"/repo/pkg/__init__.py".startsWith("repo/pkg/")` === false → blocked, so
// the suffix-only file `/repo/vendor/pkg/thing.py` stays unresolved.
expect(
resolve(
'/repo/app/main.py',
['/repo/pkg/__init__.py', '/repo/vendor/pkg/thing.py'],
'pkg.thing',
),
).toBeNull();
// Control: the SAME shape with repo-relative paths (what production emits)
// gates through and resolves — proving the fix only blocks the absolute-path
// false positive, not the real relative case.
expect(
resolve(
'repo/app/main.py',
['repo/pkg/__init__.py', 'repo/vendor/pkg/thing.py'],
'pkg.thing',
),
).toBe('repo/vendor/pkg/thing.py');
});
it('ignores non-.py files in a polyglot file set (PR #1918 P3b)', () => {
// The index is .py-only; sibling .ts/.go files of the same basename must not
// affect resolution. `pkg.models` resolves to the .py, never the .ts/.go.
const files = [
'pkg/__init__.py',
'a/pkg/models.py',
'a/pkg/models.ts',
'b/pkg/models.go',
'a/pkg/helper.ts',
];
expect(resolve('app/main.py', files, 'pkg.models')).toBe('a/pkg/models.py');
// A package whose only file is non-.py is not a repo candidate → null.
expect(resolve('app/main.py', [...files, 'tsonly/widget.ts'], 'tsonly.widget')).toBeNull();
});
});

View file

@ -0,0 +1,254 @@
/**
* Golden capture-parity test for `emitRubyScopeCaptures` (issue #1848 follow-up,
* PR #1918).
*
* Pins the exact capture output of `emitRubyScopeCaptures` across the whole
* `test/fixtures/lang-resolution/ruby-*` corpus plus a synthetic generated-DAO
* source, so any future drift in the Ruby scope-capture path fails CI rather
* than only being caught by a coarse perf tripwire or the bench-harness
* fingerprint (which is not committed as a test).
*
* This is a FORWARD-DRIFT guard: it locks in the current (post-#1918, verified)
* output as the baseline. It does not independently re-prove the original
* pre-fix parity — that was established during PR #1918.
*
* Regenerate the golden intentionally with `UPDATE_GOLDEN=1` in the environment.
*
* Per fixture the snapshot stores `{ captureGroups, digest }`:
* - captureGroups: number of capture matches (makes a count change legible)
* - digest: sha256 of a match-grouped, order-sensitive (emission-order)
* canonicalization (see canonicalize* below). Order-sensitivity is safe
* because emitRubyScopeCaptures output is deterministic, and it makes the
* digest a true byte-identical guard (a reordering refactor is real drift).
* Nothing path/time/id-dependent leaks in.
*
* Pattern: mirrors test/unit/scope-resolution/go/go-captures-golden.test.ts.
*/
import { describe, it, expect } from 'vitest';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { emitRubyScopeCaptures } from '../../../../src/core/ingestion/languages/ruby/index.js';
import type { CaptureMatch } from 'gitnexus-shared';
// This test lives at test/unit/scope-resolution/ruby/, so fixtures are THREE
// levels up (like the sibling go-captures-golden.test.ts).
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'lang-resolution');
const GOLDEN_DIR = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'ruby-captures-golden');
const GOLDEN_FILE = path.join(GOLDEN_DIR, 'expected-captures.json');
const UPDATE = process.env.UPDATE_GOLDEN === '1';
interface FixtureSnapshot {
captureGroups: number;
digest: string;
}
type Snapshot = Record<string, FixtureSnapshot>;
/**
* Canonicalize ONE match. A CaptureMatch is a Record<tag, Capture> (multiple
* captures per match), so we group by match to preserve match identity:
* build one `tag|text|startLine:startCol-endLine:endCol` string per capture,
* sort them within the match, and join. We deliberately do NOT flatten every
* capture into one global list — that would lose match boundaries.
*/
function canonicalizeMatch(match: CaptureMatch): string {
const parts: string[] = [];
for (const tag of Object.keys(match)) {
const cap = match[tag]!;
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
/** Order-sensitive (emission-order) digest of a full capture result (match-grouped). */
function digestCaptures(matches: readonly CaptureMatch[]): string {
// No cross-match sort: the digest reflects emission order so a reordering
// refactor surfaces as drift. Within-match key order IS normalized
// (canonicalizeMatch sorts), since a CaptureMatch is an unordered Record.
const matchStrings = matches.map(canonicalizeMatch);
return crypto.createHash('sha256').update(matchStrings.join('\n')).digest('hex');
}
function snapshotOf(src: string, filePath: string): FixtureSnapshot {
const matches = emitRubyScopeCaptures(src, filePath);
return { captureGroups: matches.length, digest: digestCaptures(matches) };
}
/** All `.rb` files under `lang-resolution/ruby-*`, as sorted repo-relative-ish keys. */
function collectRubyFixtures(): { key: string; absPath: string }[] {
const out: { key: string; absPath: string }[] = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('ruby-')) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop()!;
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (c.name.endsWith('.rb')) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
/**
* Small deterministic generated-DAO source — the #1848 shape at correctness
* scale. Mirrors the `ruby` unit shape in bench/scope-capture/measure.mjs.
*/
function generateDao(entityCount: number): string {
let src = '';
for (let i = 0; i < entityCount; i++) {
src +=
`class Entity${i}\n` +
` def get_id\n @id\n end\n` +
` def set_name(v)\n @name = v\n end\nend\n\n`;
}
return src;
}
function buildSnapshot(): Snapshot {
const snap: Snapshot = {};
for (const { key, absPath } of collectRubyFixtures()) {
snap[key] = snapshotOf(fs.readFileSync(absPath, 'utf8'), absPath);
}
snap['synthetic:dao-20'] = snapshotOf(generateDao(20), 'zz_generated_dao.rb');
// Stable key order for deterministic JSON serialization.
return Object.fromEntries(
Object.keys(snap)
.sort()
.map((k) => [k, snap[k]!]),
);
}
function formatGolden(snap: Snapshot): string {
return JSON.stringify(snap, null, 2) + '\n';
}
/**
* Pure decision for what the golden test should do — extracted so the
* fail-on-missing-in-CI rule is unit-testable without touching the filesystem
* (and can never corrupt the committed golden). A missing golden must NOT
* self-heal in CI; locally it regenerates as a first-run convenience.
*/
type GoldenAction = 'regenerate' | 'compare' | 'fail';
function resolveGoldenAction(opts: {
update: boolean;
exists: boolean;
isCI: boolean;
}): GoldenAction {
if (opts.update) return 'regenerate';
if (!opts.exists) return opts.isCI ? 'fail' : 'regenerate';
return 'compare';
}
describe('Ruby scope captures — golden parity', () => {
it('matches the committed golden snapshot across all ruby-* fixtures + DAO shape', () => {
const snapshot = buildSnapshot();
// Read the golden once (no existsSync-then-use, which is a TOCTOU race):
// ENOENT means the golden is missing; reuse `existing` for the compare path.
let existing: string | undefined;
try {
existing = fs.readFileSync(GOLDEN_FILE, 'utf8');
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
}
const action = resolveGoldenAction({
update: UPDATE,
exists: existing !== undefined,
isCI: !!process.env.CI, // truthy check: fires on any CI runner, not just CI==='true'
});
if (action === 'fail') {
throw new Error(
`[ruby-captures-golden] golden file missing at ${GOLDEN_FILE} in CI. A missing golden must ` +
`not self-heal in CI — regenerate it locally with UPDATE_GOLDEN=1 and commit it.`,
);
}
if (action === 'regenerate') {
fs.mkdirSync(GOLDEN_DIR, { recursive: true });
fs.writeFileSync(GOLDEN_FILE, formatGolden(snapshot), 'utf8');
console.log(
`[ruby-captures-golden] ${UPDATE ? 'Regenerated' : 'Created'} golden at ${GOLDEN_FILE}`,
);
return;
}
const expected: Snapshot = JSON.parse(existing!);
expect(
snapshot,
'emitRubyScopeCaptures output drifted from the committed golden. If this drift is intentional ' +
'(or the digest scheme changed), regenerate with ' +
'UPDATE_GOLDEN=1 npx vitest run test/unit/scope-resolution/ruby/ruby-captures-golden.test.ts',
).toEqual(expected);
});
// The fail-on-missing-in-CI rule, asserted purely (no filesystem mutation).
it.each([
{ update: true, exists: false, isCI: true, expected: 'regenerate' },
{ update: false, exists: false, isCI: true, expected: 'fail' },
{ update: false, exists: false, isCI: false, expected: 'regenerate' },
{ update: false, exists: true, isCI: true, expected: 'compare' },
{ update: false, exists: true, isCI: false, expected: 'compare' },
])(
'resolveGoldenAction($update,$exists,$isCI) -> $expected',
({ update, exists, isCI, expected }) => {
expect(resolveGoldenAction({ update, exists, isCI })).toBe(expected);
},
);
it('produces a deterministic digest across repeated runs', () => {
const src = generateDao(8);
expect(digestCaptures(emitRubyScopeCaptures(src, 'a.rb'))).toBe(
digestCaptures(emitRubyScopeCaptures(src, 'a.rb')),
);
});
it('digest is sensitive to capture-match emission order', () => {
const matches = emitRubyScopeCaptures(generateDao(6), 'a.rb');
expect(matches.length).toBeGreaterThan(1);
const reversed = [...matches].reverse();
// Reordering the emission changes the digest — the true byte-identical guard.
expect(digestCaptures(reversed)).not.toBe(digestCaptures(matches));
});
it('records a capture-group count for every fixture and the DAO shape', () => {
const snapshot = buildSnapshot();
const fixtureKeys = collectRubyFixtures().map((f) => f.key);
// Every collected fixture is present in the snapshot.
for (const k of fixtureKeys) expect(snapshot[k]).toBeDefined();
// The DAO shape (which has symbols) yields a non-empty capture set.
expect(snapshot['synthetic:dao-20']!.captureGroups).toBeGreaterThan(0);
});
// PR #1918 P3 dedup regression: the constructor-return inference dedup is now
// computed from a snapshot of the YARD-pass return keys, not a LIVE `out.some`
// that also saw bindings this very loop pushed. The old code could
// cross-suppress the 2nd of two same-named methods one source row apart whose
// bodies both end in `Const.new`. Pin that BOTH bindings are emitted now.
it('emits both constructor-return bindings for two same-named methods (no cross-suppression)', () => {
const src =
'class A\n def foo\n X.new\n end\nend\n' + 'class B\n def foo\n Y.new\n end\nend\n';
const matches = emitRubyScopeCaptures(src, 'dedup.rb');
const returnBindings = matches.filter(
(m) =>
m['@type-binding.return'] !== undefined &&
m['@type-binding.name'] !== undefined &&
m['@type-binding.name']!.text === 'foo',
);
// Both foo->X and foo->Y must survive; the old live-some dedup dropped one.
expect(returnBindings.length).toBe(2);
const boundTypes = returnBindings.map((m) => m['@type-binding.type']!.text).sort();
expect(boundTypes).toEqual(['X', 'Y']);
});
});

View file

@ -0,0 +1,233 @@
/**
* Golden capture-parity test for `emitRustScopeCaptures` (PR #1918 follow-up).
*
* Pins the exact capture output of `emitRustScopeCaptures` across the whole
* `test/fixtures/lang-resolution/rust-*` corpus plus a synthetic generated-DAO
* source, so any future drift in the Rust scope-capture path fails CI rather
* than only being caught by a coarse perf tripwire or the CI-job fingerprint.
*
* This is a FORWARD-DRIFT guard: it locks in the current (post-#1918, verified)
* output as the baseline. It does not independently re-prove the original
* pre-fix parity — that was established during PR #1918.
*
* Regenerate the golden intentionally with `UPDATE_GOLDEN=1` in the environment.
*
* Per fixture the snapshot stores `{ captureGroups, digest }`:
* - captureGroups: number of capture matches (makes a count change legible)
* - digest: sha256 of a match-grouped, order-sensitive (emission-order)
* canonicalization (see canonicalize* below). Order-sensitivity is safe
* because emitRustScopeCaptures output is deterministic, and it makes the
* digest a true byte-identical guard (a reordering refactor is real drift).
* Nothing path/time/id-dependent leaks in.
*
* Pattern: mirrors test/unit/scope-resolution/go/go-captures-golden.test.ts.
*/
import { describe, it, expect } from 'vitest';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { emitRustScopeCaptures } from '../../../../src/core/ingestion/languages/rust/index.js';
import type { CaptureMatch } from 'gitnexus-shared';
// This test lives at test/unit/scope-resolution/rust/, so fixtures are THREE
// levels up (mirrors go-captures-golden.test.ts).
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'lang-resolution');
const GOLDEN_DIR = path.resolve(__dirname, '..', '..', '..', 'fixtures', 'rust-captures-golden');
const GOLDEN_FILE = path.join(GOLDEN_DIR, 'expected-captures.json');
const UPDATE = process.env.UPDATE_GOLDEN === '1';
interface FixtureSnapshot {
captureGroups: number;
digest: string;
}
type Snapshot = Record<string, FixtureSnapshot>;
/**
* Canonicalize ONE match. A CaptureMatch is a Record<tag, Capture> (multiple
* captures per match), so we group by match to preserve match identity:
* build one `tag|text|startLine:startCol-endLine:endCol` string per capture,
* sort them within the match, and join. We deliberately do NOT flatten every
* capture into one global list — that would lose match boundaries.
*/
function canonicalizeMatch(match: CaptureMatch): string {
const parts: string[] = [];
for (const tag of Object.keys(match)) {
const cap = match[tag]!;
const r = cap.range;
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
}
parts.sort();
return parts.join(';');
}
/** Order-sensitive (emission-order) digest of a full capture result (match-grouped). */
function digestCaptures(matches: readonly CaptureMatch[]): string {
// No cross-match sort: the digest reflects emission order so a reordering
// refactor surfaces as drift. Within-match key order IS normalized
// (canonicalizeMatch sorts), since a CaptureMatch is an unordered Record.
const matchStrings = matches.map(canonicalizeMatch);
return crypto.createHash('sha256').update(matchStrings.join('\n')).digest('hex');
}
function snapshotOf(src: string, filePath: string): FixtureSnapshot {
const matches = emitRustScopeCaptures(src, filePath);
return { captureGroups: matches.length, digest: digestCaptures(matches) };
}
/** All `.rs` files under `lang-resolution/rust-*`, as sorted repo-relative-ish keys. */
function collectRustFixtures(): { key: string; absPath: string }[] {
const out: { key: string; absPath: string }[] = [];
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('rust-')) continue;
const stack = [path.join(FIXTURE_ROOT, entry.name)];
while (stack.length) {
const dir = stack.pop()!;
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, c.name);
if (c.isDirectory()) stack.push(p);
else if (c.name.endsWith('.rs')) {
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
}
}
}
}
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
/**
* Small deterministic generated-DAO source — the #1848-shape at correctness
* scale. Mirrors the rust `unit` shape in bench/scope-capture/measure.mjs.
* The `fn get_id(&self) -> i64` impl method ensures the @type-binding.return
* impl-hoist path is exercised by the golden.
*/
function generateDao(entityCount: number): string {
let src = '';
for (let i = 0; i < entityCount; i++) {
src +=
`struct Entity${i} {\n id: i64,\n name: String,\n}\n\n` +
`impl Entity${i} {\n` +
` fn get_id(&self) -> i64 { self.id }\n` +
` fn set_name(&mut self, v: String) { self.name = v; }\n}\n\n`;
}
return src;
}
function buildSnapshot(): Snapshot {
const snap: Snapshot = {};
for (const { key, absPath } of collectRustFixtures()) {
snap[key] = snapshotOf(fs.readFileSync(absPath, 'utf8'), absPath);
}
snap['synthetic:dao-20'] = snapshotOf(generateDao(20), 'zz_generated_dao.rs');
// Stable key order for deterministic JSON serialization.
return Object.fromEntries(
Object.keys(snap)
.sort()
.map((k) => [k, snap[k]!]),
);
}
function formatGolden(snap: Snapshot): string {
return JSON.stringify(snap, null, 2) + '\n';
}
/**
* Pure decision for what the golden test should do — extracted so the
* fail-on-missing-in-CI rule is unit-testable without touching the filesystem
* (and can never corrupt the committed golden). A missing golden must NOT
* self-heal in CI; locally it regenerates as a first-run convenience.
*/
type GoldenAction = 'regenerate' | 'compare' | 'fail';
function resolveGoldenAction(opts: {
update: boolean;
exists: boolean;
isCI: boolean;
}): GoldenAction {
if (opts.update) return 'regenerate';
if (!opts.exists) return opts.isCI ? 'fail' : 'regenerate';
return 'compare';
}
describe('Rust scope captures — golden parity', () => {
it('matches the committed golden snapshot across all rust-* fixtures + DAO shape', () => {
const snapshot = buildSnapshot();
// Read the golden once (no existsSync-then-use, which is a TOCTOU race):
// ENOENT means the golden is missing; reuse `existing` for the compare path.
let existing: string | undefined;
try {
existing = fs.readFileSync(GOLDEN_FILE, 'utf8');
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
}
const action = resolveGoldenAction({
update: UPDATE,
exists: existing !== undefined,
isCI: !!process.env.CI, // truthy check: fires on any CI runner, not just CI==='true'
});
if (action === 'fail') {
throw new Error(
`[rust-captures-golden] golden file missing at ${GOLDEN_FILE} in CI. A missing golden must ` +
`not self-heal in CI — regenerate it locally with UPDATE_GOLDEN=1 and commit it.`,
);
}
if (action === 'regenerate') {
fs.mkdirSync(GOLDEN_DIR, { recursive: true });
fs.writeFileSync(GOLDEN_FILE, formatGolden(snapshot), 'utf8');
console.log(
`[rust-captures-golden] ${UPDATE ? 'Regenerated' : 'Created'} golden at ${GOLDEN_FILE}`,
);
return;
}
const expected: Snapshot = JSON.parse(existing!);
expect(
snapshot,
'emitRustScopeCaptures output drifted from the committed golden. If this drift is intentional ' +
'(or the digest scheme changed), regenerate with ' +
'UPDATE_GOLDEN=1 npx vitest run test/unit/scope-resolution/rust/rust-captures-golden.test.ts',
).toEqual(expected);
});
// The fail-on-missing-in-CI rule, asserted purely (no filesystem mutation).
it.each([
{ update: true, exists: false, isCI: true, expected: 'regenerate' },
{ update: false, exists: false, isCI: true, expected: 'fail' },
{ update: false, exists: false, isCI: false, expected: 'regenerate' },
{ update: false, exists: true, isCI: true, expected: 'compare' },
{ update: false, exists: true, isCI: false, expected: 'compare' },
])(
'resolveGoldenAction($update,$exists,$isCI) -> $expected',
({ update, exists, isCI, expected }) => {
expect(resolveGoldenAction({ update, exists, isCI })).toBe(expected);
},
);
it('produces a deterministic digest across repeated runs', () => {
const src = generateDao(8);
expect(digestCaptures(emitRustScopeCaptures(src, 'a.rs'))).toBe(
digestCaptures(emitRustScopeCaptures(src, 'a.rs')),
);
});
it('digest is sensitive to capture-match emission order', () => {
const matches = emitRustScopeCaptures(generateDao(6), 'a.rs');
expect(matches.length).toBeGreaterThan(1);
const reversed = [...matches].reverse();
// Reordering the emission changes the digest — the true byte-identical guard.
expect(digestCaptures(reversed)).not.toBe(digestCaptures(matches));
});
it('records a capture-group count for every fixture and the DAO shape', () => {
const snapshot = buildSnapshot();
const fixtureKeys = collectRustFixtures().map((f) => f.key);
// Every collected fixture is present in the snapshot.
for (const k of fixtureKeys) expect(snapshot[k]).toBeDefined();
// The DAO shape (which has symbols) yields a non-empty capture set.
expect(snapshot['synthetic:dao-20']!.captureGroups).toBeGreaterThan(0);
});
});