From e50c49949c628a3c4f0d19e966dbd7c7f35477da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:18:38 +0000 Subject: [PATCH 1/3] chore(deps): bump softprops/action-gh-release from 3.0.1 to 3.0.2 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/718ea10b132b3b2eba29c1007bb80653f286566b...3d0d9888cb7fd7b750713d6e236d1fcb99157228) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e201303bf..ce924241d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -828,7 +828,7 @@ jobs: fi - name: Create GitHub Release - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v2 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v2 with: tag_name: ${{ steps.vtag-gate.outputs.vtag }} name: >- From 76f9f70183abc5825a70c41906393ebfe2dd432f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Thu, 23 Jul 2026 11:59:56 +0100 Subject: [PATCH 2/3] fix(cli): LadybugDB native-load failures fail closed, incl. truncated-binary SIGBUS (#2441) (#2651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(cli): cover analyzer lazy-action native-load failure (#2441) createAnalyzerLbugLazyAction — the wrapper the `analyze` command uses — had only a happy-path test; its native-load-failure branch was untested, so a regression could silently reintroduce #2441 (analyze exiting 0 after a LadybugDB native load failure, writing no index while reporting success). Add a failure-path test asserting that when checkLbugNative() reports the binary cannot load, the analyzer module is NOT imported, process.exitCode is set to 1, and the repair message is written to stderr. Mirrors the existing createLbugLazyAction failure test. Verified discriminating: the test fails ("expected undefined to be 1") when the exitCode guard is removed from the analyzer branch, and passes with it restored. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(cli): probe LadybugDB native load out-of-process so a truncated binary fails closed (#2441) checkLbugNative() loaded lbugjs.node in-process to validate it. That catches clean load failures (missing dylib, zero-byte, garbage -> "file too short"), but a merely truncated/corrupted binary (valid header, missing pages) SIGBUSes the dynamic loader mid-dlopen — a signal, not a catchable throw — taking the whole CLI down with a raw exit 135 and no guidance. Load the binary in a throwaway child process instead. Only a child that RAN and failed (non-zero exit or a fatal signal) marks the binary bad; if the probe itself could not run — a spawn error or timeout, e.g. a no-subprocess sandbox or a non-Node execPath — the result is inconclusive and the command's own load stays authoritative rather than condemning a healthy binary. The probe forces ELECTRON_RUN_AS_NODE, removes the redundant in-process pre-load, and costs ~20ms. Regression tests: truncated binary -> ok:false; unspawnable probe -> ok:true. Verified: a 300KB-truncated native now exits 1 with the repair message (previously exit 135 SIGBUS); zero-byte/garbage stay graceful; good native still loads and indexes. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- gitnexus/src/core/lbug/native-check.ts | 97 ++++++++++++++------ gitnexus/test/unit/lazy-action.test.ts | 42 +++++++++ gitnexus/test/unit/lbug-native-check.test.ts | 45 +++++++++ 3 files changed, 157 insertions(+), 27 deletions(-) diff --git a/gitnexus/src/core/lbug/native-check.ts b/gitnexus/src/core/lbug/native-check.ts index a9971a5e9..c874ed98e 100644 --- a/gitnexus/src/core/lbug/native-check.ts +++ b/gitnexus/src/core/lbug/native-check.ts @@ -1,6 +1,11 @@ import fs from 'fs'; import path from 'path'; import { createRequire } from 'node:module'; +import { spawnSync, type SpawnSyncReturns } from 'node:child_process'; + +/** Cap the out-of-process native load probe so a hung filesystem cannot wedge a + * CLI startup gate (same bounding rationale as the extension probe below). */ +const NATIVE_LOAD_PROBE_TIMEOUT_MS = 15_000; export interface NativeCheckResult { ok: boolean; @@ -59,35 +64,73 @@ export function checkLbugNative(overridePkgDir?: string): NativeCheckResult { }; } - try { - const _require = createRequire(import.meta.url); - _require(binaryPath); - } catch (err: unknown) { - const nativeError = err instanceof Error ? err.message : String(err); - return { - ok: false, - binaryPath, - message: [ - 'LadybugDB native binary (lbugjs.node) exists but failed to load:', - ` ${nativeError}`, - '', - 'This can happen with a truncated file, ABI mismatch, or wrong-platform binary.', - '', - 'To repair:', - ` node ${path.join(pkgDir, 'install.js')}`, - '', - 'If install scripts were skipped (pnpm dlx / pnpx / ignore-scripts):', - ' pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter \\', - ' dlx gitnexus@latest serve', - ' pnpm add -g --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter gitnexus', - '', - 'If using bun, add to package.json and reinstall:', - ' "trustedDependencies": ["@ladybugdb/core"]', - ].join('\n'), - }; + // Validate loadability in a THROWAWAY CHILD PROCESS, not in-process. A merely + // truncated or corrupted .node (valid header, missing pages) does not throw a + // catchable error — it SIGBUSes the dynamic loader mid-dlopen, which would take + // the whole CLI down with a raw exit 135 and no guidance (#2441). Loading it in + // a child lets us observe that crash (a non-zero exit or a kill signal) and turn + // it into the same actionable failure as a clean load error. The child requires + // the binary by absolute path, exactly as the former in-process load did. + const probe = spawnSync(process.execPath, ['-e', 'require(process.argv[1])', binaryPath], { + encoding: 'utf8', + timeout: NATIVE_LOAD_PROBE_TIMEOUT_MS, + stdio: ['ignore', 'ignore', 'pipe'], + // Run as Node even if process.execPath is an Electron/embedder binary. + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, + }); + + // Only a child that actually RAN and failed proves the binary is bad. If the + // probe could not run at all — a spawn error or a timeout, e.g. a sandbox that + // forbids subprocesses or a non-Node execPath — we could not test the binary, + // so we stay out of the way and let the command's own load be the authority + // rather than condemn a healthy binary. (#2441 still holds: a genuinely broken + // binary loaded in-process later still exits non-zero.) + if (probe.error || probe.status === 0) { + return { ok: true, binaryPath }; } - return { ok: true, binaryPath }; + return { + ok: false, + binaryPath, + message: [ + 'LadybugDB native binary (lbugjs.node) exists but failed to load:', + ` ${describeNativeLoadFailure(probe)}`, + '', + 'This can happen with a truncated file, ABI mismatch, or wrong-platform binary.', + '', + 'To repair:', + ` node ${path.join(pkgDir, 'install.js')}`, + '', + 'If install scripts were skipped (pnpm dlx / pnpx / ignore-scripts):', + ' pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter \\', + ' dlx gitnexus@latest serve', + ' pnpm add -g --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter gitnexus', + '', + 'If using bun, add to package.json and reinstall:', + ' "trustedDependencies": ["@ladybugdb/core"]', + ].join('\n'), + }; +} + +/** + * Describe a child-observed native load failure. Reached only after a probe that + * actually ran and failed: a fatal signal (SIGBUS/SIGSEGV ⇒ truncated/corrupt + * binary), otherwise the child's own load error lifted from its stderr. + */ +function describeNativeLoadFailure(probe: SpawnSyncReturns): string { + if (probe.signal) { + return `crashed while loading (signal ${probe.signal}) — the binary is likely truncated or corrupted`; + } + const lines = (probe.stderr ?? '') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + const errorLine = lines.find((line) => /^\w*Error: /.test(line)); + return ( + errorLine?.replace(/^\w*Error:\s*/, '') ?? + lines.at(-1) ?? + `exited with code ${probe.status ?? 'unknown'}` + ); } export interface FtsProbeResult { diff --git a/gitnexus/test/unit/lazy-action.test.ts b/gitnexus/test/unit/lazy-action.test.ts index 9afe7bc27..973d728ca 100644 --- a/gitnexus/test/unit/lazy-action.test.ts +++ b/gitnexus/test/unit/lazy-action.test.ts @@ -90,4 +90,46 @@ describe('createAnalyzerLbugLazyAction', () => { expect(events).toEqual(['identity-module', 'receipt-captured', 'analyzer-module']); expect(run).toHaveBeenCalledWith(receipt, 'repo', { force: true }); }); + + it('sets exit code 1 and skips the analyzer import when native load fails', async () => { + // Regression guard for #2441: a LadybugDB native-load failure must fail + // closed — no analyzer import, no index write, non-zero exit — not the + // pre-fix "print help then exit 0" silent success. Mirrors the + // createLbugLazyAction failure test above for the analyze-only wrapper. + checkLbugNativeMock.mockReturnValueOnce({ + ok: false, + message: + 'LadybugDB native binary (lbugjs.node) exists but failed to load:\n' + ' dlopen failed', + }); + const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + process.exitCode = undefined; + const run = vi.fn(async () => undefined); + const analyzerLoader = vi.fn(async () => ({ run })); + const identityLoader = vi.fn(async () => ({ + captureAnalyzerIdentityBeforeLoad: async (_url: string, loader: () => Promise) => { + const loaded = await loader(); + return { runnerIdentity: { schemaVersion: 4 }, loaded }; + }, + })); + const action = createAnalyzerLbugLazyAction( + identityLoader as never, + analyzerLoader, + 'run', + 'file:///fixture/dist/cli/index.js', + ); + + try { + await expect(action('repo', { force: true })).resolves.toBeUndefined(); + + expect(analyzerLoader).not.toHaveBeenCalled(); + expect(run).not.toHaveBeenCalled(); + expect(process.exitCode).toBe(1); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('LadybugDB native binary (lbugjs.node) exists but failed to load:'), + ); + } finally { + stderrSpy.mockRestore(); + process.exitCode = undefined; + } + }); }); diff --git a/gitnexus/test/unit/lbug-native-check.test.ts b/gitnexus/test/unit/lbug-native-check.test.ts index c54b1635b..20883a77e 100644 --- a/gitnexus/test/unit/lbug-native-check.test.ts +++ b/gitnexus/test/unit/lbug-native-check.test.ts @@ -49,4 +49,49 @@ describe('checkLbugNative', () => { await fs.rm(tmpDir, { recursive: true, force: true }); } }); + + it('returns ok:false when lbugjs.node is truncated (loader crashes with a signal)', async () => { + // A partially written .node (valid header, missing pages) SIGBUSes dlopen — a + // signal, not a catchable throw. The out-of-process probe must observe the + // crash and report it, instead of the whole process dying with exit 135 (#2441). + const realPath = checkLbugNative().binaryPath; + expect(realPath).toBeDefined(); + const truncated = (await fs.readFile(realPath!)).subarray(0, 300_000); + + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-')); + try { + await fs.writeFile(path.join(tmpDir, 'install.js'), ''); + await fs.writeFile(path.join(tmpDir, 'lbugjs.node'), truncated); + + const result = checkLbugNative(tmpDir); + + expect(result.ok).toBe(false); + expect(result.message).toContain('failed to load'); + expect(result.message).toContain('install.js'); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + + it('returns ok:true when the load probe cannot be spawned (inconclusive, not a broken binary)', async () => { + // The binary is present, but the child probe cannot launch — a sandbox that + // forbids subprocesses, or a non-Node execPath. We could not test the binary, + // so a healthy one must not be condemned; the command's own load stays + // authoritative. (Binary content is irrelevant here — the probe never runs.) + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-')); + const originalExecPath = process.execPath; + try { + await fs.writeFile(path.join(tmpDir, 'lbugjs.node'), Buffer.from('content-irrelevant')); + await fs.writeFile(path.join(tmpDir, 'install.js'), ''); + process.execPath = path.join(tmpDir, 'definitely-not-node'); + + const result = checkLbugNative(tmpDir); + + expect(result.ok).toBe(true); + expect(result.message).toBeUndefined(); + } finally { + process.execPath = originalExecPath; + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); }); From 170805647c0e735eb2d4c490ed8ca563b0450066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Thu, 23 Jul 2026 13:43:24 +0100 Subject: [PATCH 3/3] fix(rust): keep duplicate type names ambiguous in range binding (#2514) (#2652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(rust): latch duplicate type-name ambiguity in range binding (#2514) The range-binding prepass tracked cross-file return and field types in two maps and used map presence itself as the ambiguity flag: the second definition of a name deleted it, but a third definition found it absent and re-inserted the last-scanned file's type. Odd duplicate counts (3, 5, ...) therefore resolved a genuinely ambiguous name to whichever file was scanned last, while even counts stayed ambiguous. Latch ambiguity in a dedicated Set per registry (ambiguousReturnTypes, ambiguousFieldTypes): once a name has two or more workspace definitions it never resolves again, regardless of duplicate count or file order. Adds integration coverage for two/three-duplicate functions and structs, permuted file order, and a unique-name over-suppression guard. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(rust): bump INCREMENTAL_SCHEMA_VERSION to 12 for the #2514 range-binding fix The duplicate-name ambiguity latch changes which cross-file Rust CALLS edges the range-binding prepass emits. The incremental writeback persists only changed-file nodes, so an incremental top-up against a pre-v12 index would keep the old spurious edges on every unchanged Rust file. Bump the schema version to force a one-time full re-analyze, matching the v7/v11 contract for edge-affecting resolver changes. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(rust): resolve import-disambiguated duplicate types in for-loops & destructuring Follow-up to the #2514 ambiguity latch. When several modules define the same function/struct name and a call site disambiguates it with a `use` import (including aliases and `use x::*` globs), range-binding now resolves the for-loop element type and the destructured field type to that specific imported definition, instead of leaving it unresolved. The bare-name return/field maps are (correctly) ambiguous for duplicates, but the call site's import pins a definition. range-binding records the full, untruncated return/field type per defining file, and resolveImportedDef() resolves a name to the single in-scope definition, mirroring Rust name resolution: - tier 1: explicit `use`/re-export imports and local defs (lookupBindingsAt); these shadow globs, so if any exist we decide within them alone; - tier 2: glob imports, consulted only when tier 1 is empty; a `wildcard-expanded` ImportEdge names the target module, so we resolve only when exactly one glob-target file actually defines the name. Two or more visible definitions stay unresolved, preserving the #2514 latch. normalizeRustReturnType is untouched (its Vec -> Vec truncation is load-bearing for receiver resolution), so the full generic is read from the per-file map instead. Covered by integration tests: explicit / aliased / single-glob imports resolve to the imported definition; two globs that both export the name stay ambiguous; a local definition shadows a glob; no-import duplicates stay unresolved (#2514). INCREMENTAL_SCHEMA_VERSION stays at 12 (bumped by the #2514 commit in this PR); its note now also covers these added resolution edges. Co-Authored-By: Claude Opus 4.8 (1M context) * perf(rust): parse each file once in range-binding when the workspace fits a budget populateRustRangeBindings makes two passes over every file and, because the shared treeCache is empty in the analyze flow, re-parsed each file in both — a workspace of N files paid 2N parses. It now parses each file once and reuses the tree across both passes via an in-function store, gated by a source-byte budget: workspaces up to 16 MiB of Rust source (essentially every real repo) reuse trees; larger ones fall back to per-pass re-parsing so peak RSS stays bounded on huge repos (the memory-sensitive case keeps its current profile). Also collapses the parse+timeout boilerplate that was copy-pasted in both loops into one getOrParseTree helper, and adds a PROF-gated `rangeBind=` segment to the scope-resolution profiler for phase-level observability. Measured on a 500-file synthetic Rust workspace (PROF_SCOPE_RESOLUTION=1): the range-binding phase drops ~370ms -> ~320ms (~14%), parses 1000 -> 500. Behavior is unchanged (199 rust + range-binding-order + parse-timeout tests green); repos above the budget are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) * test(rust): update schema-version gate to v12; regenerate golden + bench baseline for new fixtures CI surfaced three deterministic-artifact failures, all from this PR's own additions: - call-summary-schema-version.test.ts hardcoded INCREMENTAL_SCHEMA_VERSION === 11 (the #2604 window); #2514 bumped it to 12. Update the gate and extend the reuse-gate version history so a v11 stamp now forces a full re-analyze. - rust-captures-golden expected-captures.json drifted (130 -> 174 entries) because the new rust-import-* / rust-dup-* fixtures joined the rust-* corpus. Regenerated (UPDATE_GOLDEN=1): additions only, no existing captures changed — emitRustScopeCaptures is untouched. - bench/scope-capture/baselines.json rust fingerprint drifted for the same reason. Rebaselined with a provenance note; scaling 1.06 < 1.5 budget. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Co-authored-by: Claude Opus 4.8 (1M context) --- gitnexus/bench/scope-capture/baselines.json | 5 +- .../ingestion/languages/rust/range-binding.ts | 256 ++++++++++++++---- .../scope-resolution/pipeline/run.ts | 2 + gitnexus/src/storage/repo-manager.ts | 15 +- .../rust-dup-fields-2/src/c_a.rs | 3 + .../rust-dup-fields-2/src/c_b.rs | 3 + .../rust-dup-fields-2/src/main.rs | 8 + .../rust-dup-fields-3/src/c_a.rs | 3 + .../rust-dup-fields-3/src/c_b.rs | 3 + .../rust-dup-fields-3/src/c_c.rs | 3 + .../rust-dup-fields-3/src/main.rs | 9 + .../rust-dup-return-2/src/main.rs | 8 + .../rust-dup-return-2/src/t_a.rs | 3 + .../rust-dup-return-2/src/t_b.rs | 3 + .../rust-dup-return-3-reordered/src/a_task.rs | 3 + .../rust-dup-return-3-reordered/src/m_repo.rs | 3 + .../rust-dup-return-3-reordered/src/main.rs | 9 + .../rust-dup-return-3-reordered/src/z_user.rs | 3 + .../rust-dup-return-3/src/main.rs | 9 + .../rust-dup-return-3/src/t_a.rs | 3 + .../rust-dup-return-3/src/t_b.rs | 3 + .../rust-dup-return-3/src/t_c.rs | 3 + .../rust-import-alias-return/src/main.rs | 10 + .../rust-import-alias-return/src/t_a.rs | 3 + .../rust-import-alias-return/src/t_b.rs | 3 + .../rust-import-alias-return/src/t_c.rs | 3 + .../rust-import-dup-fields/src/main.rs | 10 + .../rust-import-dup-fields/src/t_a.rs | 3 + .../rust-import-dup-fields/src/t_b.rs | 3 + .../rust-import-dup-fields/src/t_c.rs | 3 + .../rust-import-dup-return/src/main.rs | 10 + .../rust-import-dup-return/src/t_a.rs | 3 + .../rust-import-dup-return/src/t_b.rs | 3 + .../rust-import-dup-return/src/t_c.rs | 3 + .../rust-import-glob-ambiguous/src/main.rs | 11 + .../rust-import-glob-ambiguous/src/t_a.rs | 3 + .../rust-import-glob-ambiguous/src/t_b.rs | 3 + .../rust-import-glob-ambiguous/src/t_c.rs | 3 + .../src/main.rs | 15 + .../rust-import-glob-local-shadows/src/t_a.rs | 3 + .../rust-import-glob-local-shadows/src/t_b.rs | 3 + .../rust-import-glob-local-shadows/src/t_c.rs | 3 + .../rust-import-glob-return/src/main.rs | 10 + .../rust-import-glob-return/src/t_a.rs | 3 + .../rust-import-glob-return/src/t_b.rs | 3 + .../rust-import-glob-return/src/t_c.rs | 3 + .../rust-unique-return/src/main.rs | 7 + .../rust-unique-return/src/t_a.rs | 3 + .../expected-captures.json | 176 ++++++++++++ .../test/integration/resolvers/rust.test.ts | 209 ++++++++++++++ .../unit/call-summary-schema-version.test.ts | 11 +- 51 files changed, 821 insertions(+), 65 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/a_task.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/m_repo.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/z_user.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_a.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_b.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_c.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/main.rs create mode 100644 gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/t_a.rs diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 81e8b92ec..f55fae7cd 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -46,13 +46,14 @@ "_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11)." }, "rust": { - "fingerprint": "f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846", + "fingerprint": "655aed01cf1b6b84fa0c64d48dfb2526ecb67f47d90f0a91edabacd269a212db", "scaling_budget": 1.5, "_rebaselined_dyn_trait_object_2604": "#2604: RUST_SCOPE_QUERY now captures function_signature_item (abstract trait methods, no body) as a scope + declaration, so a &dyn Trait receiver can dispatch a CALLS edge to the trait's own method. Additive capture shift across every bench fixture with a required trait method. Prior df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29 -> f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846; scaling 1.033 < 1.5.", "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c -> df369c5a5f8de7753fc8bab8b4108ef5081750974ea5085ba9a867675ac9eb29; scaling 1.065 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Rust fn-value callable flow facts with invocation/constructor-result suppression. Prior ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82 -> 65e5bca66bb1ca117949409e8fb5c80ee69d6f1b5318908eaaecf08da0482e5c; scaling 1.024 < 1.5.", "_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) \u2014 legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.", - "_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f." + "_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures \u2014 pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f.", + "_rebaselined_import_disambiguation_2514": "#2514: added rust-import-* and rust-dup-* fixtures under lang-resolution for the range-binding ambiguity latch + import-disambiguated resolution (for-loops / struct destructuring across explicit/aliased/glob use imports). emitRustScopeCaptures is unchanged; the corpus fingerprint shifts purely because the fixture set grew (130 -> 174). Prior f7742f65f14d7d6590df7f16303fc3cc9dc0c233cd80bf90c98b084933cd3846 -> 655aed01cf1b6b84fa0c64d48dfb2526ecb67f47d90f0a91edabacd269a212db; scaling 1.06 < 1.5." }, "php": { "fingerprint": "4a688fa5a7016546f7f3c6d44de023608ae80c5b0e3670c16f6e61b3632608fd", diff --git a/gitnexus/src/core/ingestion/languages/rust/range-binding.ts b/gitnexus/src/core/ingestion/languages/rust/range-binding.ts index 4c7224333..593716cf9 100644 --- a/gitnexus/src/core/ingestion/languages/rust/range-binding.ts +++ b/gitnexus/src/core/ingestion/languages/rust/range-binding.ts @@ -5,6 +5,7 @@ import { getTreeSitterBufferSize } from '../../constants.js'; import { parseSourceSafe, ParseTimeoutError } from '../../../tree-sitter/safe-parse.js'; import type { SyntaxNode } from '../../utils/ast-helpers.js'; import { logger } from '../../../logger.js'; +import { lookupBindingsAt } from '../../scope-resolution/scope/walkers.js'; /** * Populate type bindings for patterns and iterators that the tree-sitter @@ -16,9 +17,54 @@ import { logger } from '../../../logger.js'; * Runs in Phase 2 (after propagateImportedReturnTypes) so all cross-file * type bindings are available for lookup. */ +type RustTree = ReturnType['parse']>; + +/** + * Hold parsed trees for reuse across both prepass loops only when the whole + * Rust source fits this budget. Trees are much larger than their source, so a + * modest source cap keeps peak held-tree memory bounded; larger repos fall + * back to re-parsing per loop (unchanged RSS). + */ +const TREE_REUSE_SOURCE_BUDGET_BYTES = 16 * 1024 * 1024; + +/** + * Parse `filePath`'s source once, honoring the caller's `treeCache` and, when + * provided, an in-function `store` so the two prepass loops share a single + * parse instead of re-parsing every file. Returns null when the source is + * missing or parsing times out. + */ +function getOrParseTree( + parser: ReturnType, + filePath: string, + ctx: { + readonly fileContents: ReadonlyMap; + readonly treeCache?: { get(filePath: string): unknown }; + }, + store: Map | undefined, +): RustTree | null { + const cached = (ctx.treeCache?.get(filePath) ?? store?.get(filePath)) as RustTree | undefined; + if (cached !== undefined) return cached; + const sourceText = ctx.fileContents.get(filePath); + if (sourceText === undefined) return null; + let tree: RustTree; + try { + tree = parseSourceSafe(parser, sourceText, undefined, { + bufferSize: getTreeSitterBufferSize(sourceText), + }); + } catch (err) { + if (err instanceof ParseTimeoutError) { + logger.warn({ file: filePath }, 'rust range-binding: parse timed out, skipping file'); + return null; + } + throw err; + } + store?.set(filePath, tree); + return tree; +} + export function populateRustRangeBindings( parsedFiles: readonly ParsedFile[], - _indexes: ScopeResolutionIndexes, + indexes: ScopeResolutionIndexes, ctx: { readonly fileContents: ReadonlyMap; readonly treeCache?: { get(filePath: string): unknown }; @@ -26,45 +72,45 @@ export function populateRustRangeBindings( ): void { const parser = getRustParser(); const allReturnTypes = new Map(); + const ambiguousReturnTypes = new Set(); const allFieldTypes = new Map>(); + const ambiguousFieldTypes = new Set(); + // Per-defining-file, un-collapsed, FULL-generic return/field types. When a + // bare name is ambiguous (#2514) but the call site's `use` import pins a + // single definition, we resolve that definition's file here and read its + // untruncated type so a generic `Vec` element type survives (#2514 + // follow-up: import-disambiguated duplicates resolve like the compiler). + const returnTypeByFile = new Map>(); + const fieldTypeByFile = new Map>>(); + // Parse each file once and reuse across both loops when the workspace fits + // the byte budget; otherwise re-parse per loop to bound RSS (see helper). + let totalSourceBytes = 0; + for (const parsed of parsedFiles) { + totalSourceBytes += ctx.fileContents.get(parsed.filePath)?.length ?? 0; + } + const treeStore: Map | undefined = + totalSourceBytes <= TREE_REUSE_SOURCE_BUDGET_BYTES ? new Map() : undefined; for (const parsed of parsedFiles) { - const sourceText = ctx.fileContents.get(parsed.filePath); - if (sourceText === undefined) continue; - - const cachedTree = ctx.treeCache?.get(parsed.filePath) as - | ReturnType - | undefined; - let tree: ReturnType; - if (cachedTree !== undefined) { - tree = cachedTree; - } else { - try { - tree = parseSourceSafe(parser, sourceText, undefined, { - bufferSize: getTreeSitterBufferSize(sourceText), - }); - } catch (err) { - if (err instanceof ParseTimeoutError) { - logger.warn( - { file: parsed.filePath }, - 'rust range-binding: parse timed out, skipping file', - ); - continue; - } - throw err; - } - } + const tree = getOrParseTree(parser, parsed.filePath, ctx, treeStore); + if (tree === null) continue; for (const fn of tree.rootNode.descendantsOfType('function_item')) { const nameNode = fn.childForFieldName('name'); const retType = fn.childForFieldName('return_type'); if (nameNode !== null && retType !== null) { const name = nameNode.text; + // Ambiguity is a latch, not a toggle: once a name has two or more + // workspace definitions it stays ambiguous for the rest of the + // prepass, regardless of duplicate count or file order (#2514). if (allReturnTypes.has(name)) { allReturnTypes.delete(name); - } else { + ambiguousReturnTypes.add(name); + } else if (!ambiguousReturnTypes.has(name)) { allReturnTypes.set(name, retType.text); } + // Full-generic record per defining file for import-disambiguated lookup. + recordByFile(returnTypeByFile, parsed.filePath, name, retType.text); } } @@ -82,11 +128,16 @@ export function populateRustRangeBindings( } if (fields.size > 0) { const name = nameNode.text; + // Same ambiguity latch as return types (#2514): a third same-named + // struct must not restore a resolvable global field map. if (allFieldTypes.has(name)) { allFieldTypes.delete(name); - } else { + ambiguousFieldTypes.add(name); + } else if (!ambiguousFieldTypes.has(name)) { allFieldTypes.set(name, fields); } + // Full-generic record per defining file for import-disambiguated lookup. + recordByFile(fieldTypeByFile, parsed.filePath, name, fields); } } @@ -99,39 +150,32 @@ export function populateRustRangeBindings( } for (const parsed of parsedFiles) { - const sourceText = ctx.fileContents.get(parsed.filePath); - if (sourceText === undefined) continue; - - const cachedTree = ctx.treeCache?.get(parsed.filePath) as - | ReturnType - | undefined; - let tree: ReturnType; - if (cachedTree !== undefined) { - tree = cachedTree; - } else { - try { - tree = parseSourceSafe(parser, sourceText, undefined, { - bufferSize: getTreeSitterBufferSize(sourceText), - }); - } catch (err) { - if (err instanceof ParseTimeoutError) { - logger.warn( - { file: parsed.filePath }, - 'rust range-binding: parse timed out, skipping file', - ); - continue; - } - throw err; - } - } + const tree = getOrParseTree(parser, parsed.filePath, ctx, treeStore); + if (tree === null) continue; const scopeMap = new Map(parsed.scopes.map((s) => [s.id, s])); const moduleScope = parsed.scopes.find((s) => s.kind === 'Module'); if (moduleScope === undefined) continue; - processForLoops(tree.rootNode, parsed, scopeMap, moduleScope, allReturnTypes); + processForLoops( + tree.rootNode, + parsed, + scopeMap, + moduleScope, + allReturnTypes, + indexes, + returnTypeByFile, + ); processPatternBindings(tree.rootNode, parsed, scopeMap, moduleScope); - processStructDestructuring(tree.rootNode, parsed, scopeMap, moduleScope, allFieldTypes); + processStructDestructuring( + tree.rootNode, + parsed, + scopeMap, + moduleScope, + allFieldTypes, + indexes, + fieldTypeByFile, + ); processPendingAssignments( tree.rootNode, parsed, @@ -196,12 +240,88 @@ function normalizeFieldType(text: string): string { return t.trim(); } +/** Get-or-create the inner map for `file` and record `name -> value`. */ +function recordByFile( + byFile: Map>, + file: string, + name: string, + value: V, +): void { + let inner = byFile.get(file); + if (inner === undefined) { + inner = new Map(); + byFile.set(file, inner); + } + inner.set(name, value); +} + +/** Final segment of a dot-joined qualified name (`a.make` -> `make`), or the + * bare name when the def carries no qualifier. */ +function simpleName(qualifiedName: string | undefined, bareName: string): string { + if (qualifiedName === undefined) return bareName; + const dot = qualifiedName.lastIndexOf('.'); + return dot === -1 ? qualifiedName : qualifiedName.slice(dot + 1); +} + +/** Distinct `(file, name)` definitions, in first-seen order. */ +function uniqueDefs( + defs: readonly { file: string; name: string }[], +): { file: string; name: string }[] { + const seen = new Set(); + const out: { file: string; name: string }[] = []; + for (const d of defs) { + const key = `${d.file} ${d.name}`; + if (seen.has(key)) continue; + seen.add(key); + out.push(d); + } + return out; +} + +/** + * Resolve `name` at `moduleScope` to the value recorded in `byFile` for the one + * definition visible here, or null when zero or several are visible (which + * keeps the #2514 ambiguity latch). Mirrors Rust name resolution: explicit + * `use`/re-export imports and local defs shadow `use x::*` globs, so a glob is + * consulted only when no explicit binding names `name`, and even then only when + * exactly one glob-target file actually defines it. + */ +function resolveImportedDef( + name: string, + moduleScope: Scope, + indexes: ScopeResolutionIndexes, + byFile: ReadonlyMap>, +): V | null { + const explicit = uniqueDefs( + lookupBindingsAt(moduleScope.id, name, indexes) + .filter((r) => r.origin === 'import' || r.origin === 'reexport' || r.origin === 'local') + .map((r) => ({ file: r.def.filePath, name: simpleName(r.def.qualifiedName, name) })), + ); + const defs = + explicit.length > 0 + ? explicit + : uniqueDefs( + (indexes.imports.get(moduleScope.id) ?? []) + .filter( + (e) => + e.kind === 'wildcard-expanded' && + e.targetFile !== null && + byFile.get(e.targetFile)?.has(name) === true, + ) + .map((e) => ({ file: e.targetFile as string, name })), + ); + if (defs.length !== 1) return null; + return byFile.get(defs[0].file)?.get(defs[0].name) ?? null; +} + function processForLoops( root: SyntaxNode, parsed: ParsedFile, scopeMap: ReadonlyMap, moduleScope: Scope, allReturnTypes: ReadonlyMap, + indexes: ScopeResolutionIndexes, + returnTypeByFile: ReadonlyMap>, ): void { for (const forNode of root.descendantsOfType('for_expression')) { const patternNode = forNode.childForFieldName('pattern'); @@ -217,6 +337,8 @@ function processForLoops( scopeMap, moduleScope, allReturnTypes, + indexes, + returnTypeByFile, ); if (elementType === null) continue; @@ -331,7 +453,9 @@ function processStructDestructuring( parsed: ParsedFile, scopeMap: ReadonlyMap, moduleScope: Scope, - allFieldTypes?: ReadonlyMap>, + allFieldTypes: ReadonlyMap>, + indexes: ScopeResolutionIndexes, + fieldTypeByFile: ReadonlyMap>>, ): void { for (const letNode of root.descendantsOfType('let_declaration')) { const patternNode = letNode.childForFieldName('pattern'); @@ -356,7 +480,13 @@ function processStructDestructuring( let fieldType = lookupFieldType(typeName, fieldName, parsed, scopeMap, moduleScope); if (fieldType === null) { - fieldType = allFieldTypes?.get(typeName)?.get(fieldName) ?? null; + fieldType = allFieldTypes.get(typeName)?.get(fieldName) ?? null; + } + if (fieldType === null) { + // Import-disambiguated duplicate struct (#2514 follow-up): the global + // field map is ambiguous, but a `use` import pins one definition. + const fields = resolveImportedDef(typeName, moduleScope, indexes, fieldTypeByFile); + fieldType = fields?.get(fieldName) ?? null; } if (fieldType !== null) { injectTypeBinding(targetScope, fieldName, fieldType); @@ -481,7 +611,9 @@ function resolveIterableElementType( parsed: ParsedFile, scopeMap: ReadonlyMap, moduleScope: Scope, - allReturnTypes?: ReadonlyMap, + allReturnTypes: ReadonlyMap, + indexes: ScopeResolutionIndexes, + returnTypeByFile: ReadonlyMap>, ): string | null { let iterableNode = valueNode; if (iterableNode.type === 'reference_expression') { @@ -506,10 +638,16 @@ function resolveIterableElementType( } if (func.type === 'identifier') { - const crossFileReturn = allReturnTypes?.get(func.text); + const crossFileReturn = allReturnTypes.get(func.text); if (crossFileReturn !== undefined) return unwrapGeneric(crossFileReturn); const rawReturn = lookupRawFunctionReturnType(func.text, valueNode); if (rawReturn !== null) return unwrapGeneric(rawReturn); + // Import-disambiguated duplicate: the bare-name map is ambiguous (#2514) + // but a `use` import pins one definition. Read its FULL return type + // here, BEFORE the scope-binding lookup below, because that binding is + // generic-truncated (`Vec` becomes `Vec`), losing the element. + const importedReturn = resolveImportedDef(func.text, moduleScope, indexes, returnTypeByFile); + if (importedReturn !== null) return unwrapGeneric(importedReturn); const returnType = lookupReturnTypeInScopes(func.text, parsed, scopeMap, moduleScope); if (returnType !== null) return unwrapGeneric(returnType); } diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index e47498ebb..59553b75d 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -711,6 +711,7 @@ export function runScopeResolution( propagateImportedReturnTypes(parsedFiles, indexes, workspaceIndex); } + const tRangeBindStart = PROF ? process.hrtime.bigint() : 0n; if (provider.populateRangeBindings !== undefined) { provider.populateRangeBindings(parsedFiles, indexes, { fileContents: getFileContents(), @@ -1309,6 +1310,7 @@ export function runScopeResolution( `[scope-resolution prof] extract=${ns(tStart, tExtract).toFixed(0)}ms` + ` finalize=${ns(tExtract, tFinalize).toFixed(0)}ms` + ` propagate=${ns(tFinalize, tPropagate).toFixed(0)}ms` + + ` rangeBind=${ns(tRangeBindStart, tPropagate).toFixed(1)}ms` + ` resolve=${ns(tPropagate, tResolve).toFixed(0)}ms` + ` emit=${ns(tResolve, tEnd).toFixed(0)}ms` + // pdg ⊆ emit: the M2 reaching-defs share of the emit bucket (#2082 U4). diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index 9852e8ba4..bef8a23ce 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -444,8 +444,21 @@ export interface RepoMeta { * incremental write set only covers changed files, so a top-up against a * pre-v11 index would keep silently missing these CALLS edges for every * unchanged Rust trait file; force a full re-analyze instead. + * v12: Rust range-binding stopped restoring ambiguous duplicate type names + * (#2514): a function/struct name defined three or more times used to + * re-resolve to the last-scanned file (a presence toggle), so odd duplicate + * counts emitted a wrong cross-file CALLS edge. Same v7/v11 contract: the + * incremental write set only covers changed files, so a top-up against a + * pre-v12 index would keep these spurious CALLS edges on every unchanged Rust + * file. v12 also changes edges in the other direction: range-binding now + * RESOLVES import-disambiguated duplicate names (`for item in make()` / + * `let Struct { f } = ..` where a `use` or `use x::*` import pins one of several + * same-named definitions) to the imported definition's type. Both the removed + * spurious edges and these new resolved edges are cross-file, so a pre-v12 + * top-up would leave unchanged Rust files stale either way; force a full + * re-analyze instead. */ -export const INCREMENTAL_SCHEMA_VERSION = 11; +export const INCREMENTAL_SCHEMA_VERSION = 12; export interface IndexedRepo { repoPath: string; diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_a.rs new file mode 100644 index 000000000..97e1a626c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_a.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbA } +pub struct DbA; +impl DbA { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_b.rs new file mode 100644 index 000000000..6eba40993 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/c_b.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbB } +pub struct DbB; +impl DbB { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/main.rs new file mode 100644 index 000000000..787aa4e42 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-2/src/main.rs @@ -0,0 +1,8 @@ +mod c_a; +mod c_b; +pub fn load() -> u8 { 0 } +fn use_it() { + let Config { db } = load(); + db.run(); +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_a.rs new file mode 100644 index 000000000..97e1a626c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_a.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbA } +pub struct DbA; +impl DbA { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_b.rs new file mode 100644 index 000000000..6eba40993 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_b.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbB } +pub struct DbB; +impl DbB { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_c.rs new file mode 100644 index 000000000..e90cb83a2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/c_c.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbC } +pub struct DbC; +impl DbC { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/main.rs new file mode 100644 index 000000000..92fbad3d4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-fields-3/src/main.rs @@ -0,0 +1,9 @@ +mod c_a; +mod c_b; +mod c_c; +pub fn load() -> u8 { 0 } +fn use_it() { + let Config { db } = load(); + db.run(); +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/main.rs new file mode 100644 index 000000000..9d224de54 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/main.rs @@ -0,0 +1,8 @@ +mod t_a; +mod t_b; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-2/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/a_task.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/a_task.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/a_task.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/m_repo.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/m_repo.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/m_repo.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/main.rs new file mode 100644 index 000000000..764062061 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/main.rs @@ -0,0 +1,9 @@ +mod z_user; +mod m_repo; +mod a_task; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/z_user.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/z_user.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3-reordered/src/z_user.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/main.rs new file mode 100644 index 000000000..7c2c54f02 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/main.rs @@ -0,0 +1,9 @@ +mod t_a; +mod t_b; +mod t_c; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_c.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-dup-return-3/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/main.rs new file mode 100644 index 000000000..d5efc716d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/main.rs @@ -0,0 +1,10 @@ +mod t_a; +mod t_b; +mod t_c; +use crate::t_b::make as mk; +fn drive() { + for item in mk() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_c.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-alias-return/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/main.rs new file mode 100644 index 000000000..f099c1c96 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/main.rs @@ -0,0 +1,10 @@ +mod t_a; +mod t_b; +mod t_c; +use crate::t_b::Config; +pub fn load() -> u8 { 0 } +fn use_it() { + let Config { db } = load(); + db.run(); +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_a.rs new file mode 100644 index 000000000..97e1a626c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbA } +pub struct DbA; +impl DbA { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_b.rs new file mode 100644 index 000000000..6eba40993 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbB } +pub struct DbB; +impl DbB { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_c.rs new file mode 100644 index 000000000..e90cb83a2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-fields/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Config { pub db: DbC } +pub struct DbC; +impl DbC { pub fn run(&self) {} } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/main.rs new file mode 100644 index 000000000..6645a70ca --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/main.rs @@ -0,0 +1,10 @@ +mod t_a; +mod t_b; +mod t_c; +use crate::t_b::make; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_c.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-dup-return/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/main.rs new file mode 100644 index 000000000..72d2374e4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/main.rs @@ -0,0 +1,11 @@ +mod t_a; +mod t_b; +mod t_c; +use crate::t_b::*; +use crate::t_c::*; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_c.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-ambiguous/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/main.rs new file mode 100644 index 000000000..d83ef15a8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/main.rs @@ -0,0 +1,15 @@ +mod t_a; +mod t_b; +mod t_c; +use crate::t_b::*; +pub struct Local; +impl Local { + pub fn save(&self) {} +} +pub fn make() -> Vec { vec![] } +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_c.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-local-shadows/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/main.rs new file mode 100644 index 000000000..add8f4b35 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/main.rs @@ -0,0 +1,10 @@ +mod t_a; +mod t_b; +mod t_c; +use crate::t_b::*; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_b.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_b.rs new file mode 100644 index 000000000..80c240632 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_b.rs @@ -0,0 +1,3 @@ +pub struct Repo { pub name: String } +impl Repo { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_c.rs b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_c.rs new file mode 100644 index 000000000..ac105f126 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-import-glob-return/src/t_c.rs @@ -0,0 +1,3 @@ +pub struct Task { pub name: String } +impl Task { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/main.rs b/gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/main.rs new file mode 100644 index 000000000..cedb46e85 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/main.rs @@ -0,0 +1,7 @@ +mod t_a; +fn drive() { + for item in make() { + item.save(); + } +} +fn main() {} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/t_a.rs b/gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/t_a.rs new file mode 100644 index 000000000..b62a1679d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-unique-return/src/t_a.rs @@ -0,0 +1,3 @@ +pub struct User { pub name: String } +impl User { pub fn save(&self) {} } +pub fn make() -> Vec { vec![] } diff --git a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json index f632f78aa..755630759 100644 --- a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json @@ -171,6 +171,78 @@ "captureGroups": 22, "digest": "c53db401a81fde2ffd5665393acb9cd605a62ec51c015c3aafb3f41c0897471f" }, + "rust-dup-fields-2/src/c_a.rs": { + "captureGroups": 11, + "digest": "4c7844b039d3b2c618e5e1978e0bed50a2d8de0a7a92a87fef4791c91fd2d0d2" + }, + "rust-dup-fields-2/src/c_b.rs": { + "captureGroups": 11, + "digest": "cd02dd0f2b74d8e9495634f3a33477c2b20d5f2877612733b66402eae6fe8426" + }, + "rust-dup-fields-2/src/main.rs": { + "captureGroups": 16, + "digest": "e96013ac801f874a1ad902c7bd2be277202b38fbf12030c4ebbb46edd8c0fe79" + }, + "rust-dup-fields-3/src/c_a.rs": { + "captureGroups": 11, + "digest": "4c7844b039d3b2c618e5e1978e0bed50a2d8de0a7a92a87fef4791c91fd2d0d2" + }, + "rust-dup-fields-3/src/c_b.rs": { + "captureGroups": 11, + "digest": "cd02dd0f2b74d8e9495634f3a33477c2b20d5f2877612733b66402eae6fe8426" + }, + "rust-dup-fields-3/src/c_c.rs": { + "captureGroups": 11, + "digest": "a5263afa5bc9cb6b499d3c5394cc8b0a942d9a1fe388a2366a3baca963ead634" + }, + "rust-dup-fields-3/src/main.rs": { + "captureGroups": 17, + "digest": "fad21b046b2f34b8a6fd98ffc3719446f176f7207864b3fdd9ba2d9bb07d607f" + }, + "rust-dup-return-2/src/main.rs": { + "captureGroups": 14, + "digest": "a4d637dc57a09e56dce75102c70ca6a45998f498990289855d4953bf4ed5461f" + }, + "rust-dup-return-2/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-dup-return-2/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-dup-return-3-reordered/src/a_task.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, + "rust-dup-return-3-reordered/src/m_repo.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-dup-return-3-reordered/src/main.rs": { + "captureGroups": 15, + "digest": "0f68357dbffb22af2c36ca5a935c3eea4025c119afa1a7c1d258ab9a74fe96e8" + }, + "rust-dup-return-3-reordered/src/z_user.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-dup-return-3/src/main.rs": { + "captureGroups": 15, + "digest": "a93ca0874eeaedd21dba987143fa389281d8b738612ca49c314ab91dd73e4065" + }, + "rust-dup-return-3/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-dup-return-3/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-dup-return-3/src/t_c.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, "rust-dyn-trait-object/src/lib.rs": { "captureGroups": 23, "digest": "720618dff6a43ab8e5b59aa354c0c448b9057dd6f2f7b3b22b13b82d53745943" @@ -267,6 +339,102 @@ "captureGroups": 20, "digest": "d8c1eb57431b915dd5c9055d8451054a454e340c4842628e38e4d46f69471abd" }, + "rust-import-alias-return/src/main.rs": { + "captureGroups": 16, + "digest": "c14fd2abf932f09fe19821951e73afdf03839bdb761a7d0ad347c9926a0d5542" + }, + "rust-import-alias-return/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-import-alias-return/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-import-alias-return/src/t_c.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, + "rust-import-dup-fields/src/main.rs": { + "captureGroups": 18, + "digest": "82b48d00fe2e4b5a2f52185d5d2501fdef408faf148b509fdc5f428171b9a784" + }, + "rust-import-dup-fields/src/t_a.rs": { + "captureGroups": 11, + "digest": "4c7844b039d3b2c618e5e1978e0bed50a2d8de0a7a92a87fef4791c91fd2d0d2" + }, + "rust-import-dup-fields/src/t_b.rs": { + "captureGroups": 11, + "digest": "cd02dd0f2b74d8e9495634f3a33477c2b20d5f2877612733b66402eae6fe8426" + }, + "rust-import-dup-fields/src/t_c.rs": { + "captureGroups": 11, + "digest": "a5263afa5bc9cb6b499d3c5394cc8b0a942d9a1fe388a2366a3baca963ead634" + }, + "rust-import-dup-return/src/main.rs": { + "captureGroups": 16, + "digest": "e70780bcc2ccbfca9fe2099887e30187f70c48b68a11948592778997d1e2bd13" + }, + "rust-import-dup-return/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-import-dup-return/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-import-dup-return/src/t_c.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, + "rust-import-glob-ambiguous/src/main.rs": { + "captureGroups": 17, + "digest": "31f683937b9d9208e7f2c6bd2d1092c59bcf6ab16a6a6923b48bba6fcab2a1a6" + }, + "rust-import-glob-ambiguous/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-import-glob-ambiguous/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-import-glob-ambiguous/src/t_c.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, + "rust-import-glob-local-shadows/src/main.rs": { + "captureGroups": 28, + "digest": "5242d2ee3c9bdb8c679ddc55726e423fc48997db2bcfc4f79dca3645289d1149" + }, + "rust-import-glob-local-shadows/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-import-glob-local-shadows/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-import-glob-local-shadows/src/t_c.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, + "rust-import-glob-return/src/main.rs": { + "captureGroups": 16, + "digest": "a157af0c6d7b8f9861712c1822bb5bab8e7fadbcdd58c08d4d034343117053cb" + }, + "rust-import-glob-return/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, + "rust-import-glob-return/src/t_b.rs": { + "captureGroups": 14, + "digest": "cb5f21ac23b71efdf24b121268f783d5b224e7d498ffa7a54555c97ab0f904bf" + }, + "rust-import-glob-return/src/t_c.rs": { + "captureGroups": 14, + "digest": "67e8de06340dd3b5c5c4bcc88836d1ef89b47f4a740697664dfa26cd36cf5ac5" + }, "rust-iter-for-loop/src/main.rs": { "captureGroups": 30, "digest": "529fda7f9f188814ce6044e2c99d6b005ec4a9b98835fe9609530897998b9f32" @@ -507,6 +675,14 @@ "captureGroups": 10, "digest": "e2a6fb9eab259b8c7104f1530b96b8c1f42ab32fe1d71d6bdca04d68263507f2" }, + "rust-unique-return/src/main.rs": { + "captureGroups": 13, + "digest": "72cc5728b40f51fae75359c2443986ce73f5d5450ebf8a367a2d860c87c84ed0" + }, + "rust-unique-return/src/t_a.rs": { + "captureGroups": 14, + "digest": "d531e0aec84d7da0e2c6818e445735b4cb1ff15e08d14586093069e5eeab41c9" + }, "rust-write-access/models.rs": { "captureGroups": 9, "digest": "660f755fd70cd1796f9da02ad7d65f599dea8029665ee45ecd18cd27919741f3" diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index fe970b777..2e4a400c4 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -13,6 +13,7 @@ import { edgeSet, runPipelineFromRepo, type PipelineResult, + type RelEdge, } from './helpers.js'; // --------------------------------------------------------------------------- @@ -2350,3 +2351,211 @@ describe('Rust macro resolution (issue #1934 F72)', () => { expect(calls.every((e) => e.targetLabel !== 'Macro')).toBe(true); }); }); + +// --------------------------------------------------------------------------- +// #2514: duplicate type names must stay ambiguous regardless of duplicate +// count or file order. The range-binding prepass used Map presence as an +// ambiguity toggle (has→delete / else→set), so a 3rd same-named definition +// re-inserted a resolvable — and wrong — cross-file type (the last-scanned +// file's). The fix latches ambiguity in a separate Set: once a name has two +// definitions it never resolves again. +// +// Observable: for-loop `for item in make() { item.save(); }` where each +// `make()` (or each `Config` field) lives in its own file with no `use` +// import, so the receiver type can only come from the global range-binding +// map. A cross-file `save`/`run` CALLS edge means the name resolved. +// --------------------------------------------------------------------------- + +describe('Rust duplicate-name ambiguity latch (#2514)', () => { + // Cross-file receiver-method CALLS edges emitted from the fixture driver fn. + const receiverCalls = (result: PipelineResult, source: string, method: string): RelEdge[] => + getRelationships(result, 'CALLS').filter((c) => c.source === source && c.target === method); + + // --- return-type registry (allReturnTypes) --- + + describe('two same-named fns with different return types', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-dup-return-2'), () => {}); + }, 60000); + + it('suppresses cross-file return-type inference — item.save() does not resolve', () => { + expect(receiverCalls(result, 'drive', 'save')).toEqual([]); + }); + }); + + describe('three same-named fns with different return types', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-dup-return-3'), () => {}); + }, 60000); + + it('still suppresses inference — the 3rd duplicate does not restore a binding', () => { + expect(receiverCalls(result, 'drive', 'save')).toEqual([]); + }); + }); + + describe('three same-named fns, permuted input file order', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'rust-dup-return-3-reordered'), + () => {}, + ); + }, 60000); + + it('resolution is independent of file order — still no edge', () => { + expect(receiverCalls(result, 'drive', 'save')).toEqual([]); + }); + }); + + describe('unique fn still infers normally (over-suppression guard)', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-unique-return'), () => {}); + }, 60000); + + it('resolves item.save() to User#save via cross-file return type', () => { + const edges = receiverCalls(result, 'drive', 'save'); + expect(edges.length).toBe(1); + expect(edges[0]).toMatchObject({ source: 'drive', target: 'save', targetLabel: 'Function' }); + expect(edges[0].targetFilePath).toContain('t_a.rs'); + }); + }); + + // --- field-type registry (allFieldTypes) via struct destructuring --- + + describe('two same-named structs with conflicting field types', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-dup-fields-2'), () => {}); + }, 60000); + + it('suppresses global field-type inference — db.run() does not resolve', () => { + expect(receiverCalls(result, 'use_it', 'run')).toEqual([]); + }); + }); + + describe('three same-named structs with conflicting field types', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-dup-fields-3'), () => {}); + }, 60000); + + it('still suppresses field inference — the 3rd duplicate does not restore', () => { + expect(receiverCalls(result, 'use_it', 'run')).toEqual([]); + }); + }); +}); + +// --------------------------------------------------------------------------- +// #2514 follow-up: when a `use` import disambiguates one of several same-named +// definitions, the type must resolve to THAT definition (like the compiler), +// not stay ambiguous. The bare-name map is ambiguous, but the call site's +// import pins a single defining file, so range-binding reads that definition's +// FULL return/field type — recovering generic element types the bare-name map +// would have lost. Genuinely-ambiguous (no-import) duplicates still stay +// unresolved (covered by the #2514 block above). +// --------------------------------------------------------------------------- + +describe('Rust import-disambiguated duplicate resolution (#2514 follow-up)', () => { + describe('for-loop over an imported generic-returning duplicate fn', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-import-dup-return'), () => {}); + }, 60000); + + it('resolves item.save() to the imported definition in t_b (Repo), not ambiguous', () => { + const edges = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'drive' && c.target === 'save', + ); + expect(edges.length).toBe(1); + expect(edges[0]).toMatchObject({ source: 'drive', target: 'save', targetLabel: 'Function' }); + expect(edges[0].targetFilePath).toContain('t_b.rs'); + }); + }); + + describe('struct destructuring of an imported duplicate struct', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-import-dup-fields'), () => {}); + }, 60000); + + it('resolves db.run() to the imported definition in t_b (DbB) via its field type', () => { + const edges = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'use_it' && c.target === 'run', + ); + expect(edges.length).toBe(1); + expect(edges[0]).toMatchObject({ source: 'use_it', target: 'run', targetLabel: 'Function' }); + expect(edges[0].targetFilePath).toContain('t_b.rs'); + }); + }); + + describe('aliased import (`use t_b::make as mk`) still resolves the definition', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-import-alias-return'), () => {}); + }, 60000); + + it('keys on the definition name, not the alias — item.save() resolves to t_b (Repo)', () => { + const edges = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'drive' && c.target === 'save', + ); + expect(edges.length).toBe(1); + expect(edges[0]).toMatchObject({ source: 'drive', target: 'save', targetLabel: 'Function' }); + expect(edges[0].targetFilePath).toContain('t_b.rs'); + }); + }); + + describe('single glob import (`use t_b::*`) resolves the one globbed definition', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-import-glob-return'), () => {}); + }, 60000); + + it('resolves item.save() to t_b (Repo) via the one glob-target that defines it', () => { + const edges = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'drive' && c.target === 'save', + ); + expect(edges.length).toBe(1); + expect(edges[0]).toMatchObject({ source: 'drive', target: 'save', targetLabel: 'Function' }); + expect(edges[0].targetFilePath).toContain('t_b.rs'); + }); + }); + + describe('two glob imports that both export the name stay ambiguous', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'rust-import-glob-ambiguous'), + () => {}, + ); + }, 60000); + + it('leaves item.save() unresolved when two `use x::*` both define make', () => { + const edges = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'drive' && c.target === 'save', + ); + expect(edges).toEqual([]); + }); + }); + + describe('a local definition shadows a glob import', () => { + let result: PipelineResult; + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'rust-import-glob-local-shadows'), + () => {}, + ); + }, 60000); + + it('resolves item.save() to the local make in main.rs, not the glob target', () => { + const edges = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'drive' && c.target === 'save', + ); + expect(edges.length).toBe(1); + expect(edges[0]).toMatchObject({ source: 'drive', target: 'save', targetLabel: 'Function' }); + expect(edges[0].targetFilePath).toContain('main.rs'); + }); + }); +}); diff --git a/gitnexus/test/unit/call-summary-schema-version.test.ts b/gitnexus/test/unit/call-summary-schema-version.test.ts index 2047754c4..04a53f6e1 100644 --- a/gitnexus/test/unit/call-summary-schema-version.test.ts +++ b/gitnexus/test/unit/call-summary-schema-version.test.ts @@ -73,8 +73,8 @@ describe('CALL_SUMMARY relation-type exclusion (U-C1)', () => { }); describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { - it('INCREMENTAL_SCHEMA_VERSION is bumped to 11 (Rust dyn-trait-object dispatch re-index window, #2604)', () => { - expect(INCREMENTAL_SCHEMA_VERSION).toBe(11); + it('INCREMENTAL_SCHEMA_VERSION is bumped to 12 (Rust range-binding ambiguity latch + import-disambiguated resolution, #2514)', () => { + expect(INCREMENTAL_SCHEMA_VERSION).toBe(12); }); it('a pre-current stamp fails the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate → forces full re-analyze', () => { @@ -116,7 +116,12 @@ describe('CALL_SUMMARY incremental reuse gate (U-C5)', () => { // (#2604) — abstract trait methods would keep being uncaptured (no // ownerId/CALLS resolution) on unchanged Rust trait files → must NOT reuse. expect(passesReuseGate(10)).toBe(false); + // A pre-v12 (v11) index predates the #2514 Rust range-binding fix — the + // ambiguity latch removes spurious cross-file CALLS edges and the + // import-disambiguated resolution adds new ones on unchanged Rust files, + // neither of which reach an incremental write set → must NOT reuse. + expect(passesReuseGate(11)).toBe(false); // A current-version stamp passes the gate (incremental top-up eligible). - expect(passesReuseGate(11)).toBe(true); + expect(passesReuseGate(12)).toBe(true); }); });