mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
838c164155
41 changed files with 5741 additions and 163 deletions
67
.github/workflows/ci-tests.yml
vendored
67
.github/workflows/ci-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
gitnexus/bench/python-scope/baseline-fingerprint.txt
Normal file
1
gitnexus/bench/python-scope/baseline-fingerprint.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
f2b4376f30dab76f3befc9cbd3d7cc2bf1afbd7329a5e953439083e005de4a7c
|
||||
|
|
@ -0,0 +1 @@
|
|||
d51ea9edd1902fc20dd888f3fc51907c8119a4692b92c32a427801ac7f41d451
|
||||
202
gitnexus/bench/python-scope/import-target-fingerprint.mjs
Normal file
202
gitnexus/bench/python-scope/import-target-fingerprint.mjs
Normal 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');
|
||||
}
|
||||
216
gitnexus/bench/python-scope/measure.mjs
Normal file
216
gitnexus/bench/python-scope/measure.mjs
Normal 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');
|
||||
}
|
||||
27
gitnexus/bench/scope-capture/baselines.json
Normal file
27
gitnexus/bench/scope-capture/baselines.json
Normal 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
|
||||
}
|
||||
}
|
||||
266
gitnexus/bench/scope-capture/measure.mjs
Normal file
266
gitnexus/bench/scope-capture/measure.mjs
Normal 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`);
|
||||
}
|
||||
|
|
@ -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('.')) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
29
gitnexus/src/core/ingestion/languages/python/index-stats.ts
Normal file
29
gitnexus/src/core/ingestion/languages/python/index-stats.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
634
gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json
vendored
Normal file
634
gitnexus/test/fixtures/csharp-captures-golden/expected-captures.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
55
gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp
vendored
Normal file
55
gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp
vendored
Normal 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);
|
||||
}
|
||||
554
gitnexus/test/fixtures/php-captures-golden/expected-captures.json
vendored
Normal file
554
gitnexus/test/fixtures/php-captures-golden/expected-captures.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
758
gitnexus/test/fixtures/python-captures-golden/expected-captures.json
vendored
Normal file
758
gitnexus/test/fixtures/python-captures-golden/expected-captures.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
314
gitnexus/test/fixtures/ruby-captures-golden/expected-captures.json
vendored
Normal file
314
gitnexus/test/fixtures/ruby-captures-golden/expected-captures.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
454
gitnexus/test/fixtures/rust-captures-golden/expected-captures.json
vendored
Normal file
454
gitnexus/test/fixtures/rust-captures-golden/expected-captures.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
54
gitnexus/test/integration/php-scope-capture-tripwire.test.ts
Normal file
54
gitnexus/test/integration/php-scope-capture-tripwire.test.ts
Normal 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);
|
||||
});
|
||||
87
gitnexus/test/integration/python-import-index-reuse.test.ts
Normal file
87
gitnexus/test/integration/python-import-index-reuse.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue